From c4080364ee280ab054a278115c8eacadd827dcff Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Fri, 21 Feb 2025 10:05:43 +0000 Subject: [PATCH 01/17] (#292) Uses Jenkins Job Packages --- Set-SslSecurity.ps1 | 5 +- Start-C4bJenkinsSetup.ps1 | 19 +---- files/chocolatey.json | 2 + .../builds/legacyIds | 0 .../builds/permalinks | 2 - .../config.xml | 72 ----------------- .../nextBuildNumber | 1 - .../builds/legacyIds | 0 .../builds/permalinks | 6 -- .../Update production repository/config.xml | 59 -------------- .../nextBuildNumber | 1 - .../builds/legacyIds | 0 .../builds/permalinks | 2 - .../config.xml | 43 ---------- .../nextBuildNumber | 1 - jenkins/scripts/ConvertTo-ChocoObject.ps1 | 16 ---- jenkins/scripts/Get-UpdatedPackage.ps1 | 68 ---------------- .../scripts/Invoke-ChocolateyInternalizer.ps1 | 68 ---------------- jenkins/scripts/Update-ProdRepoFromTest.ps1 | 81 ------------------- modules/C4B-Environment/C4B-Environment.psm1 | 4 +- tests/jenkins.tests.ps1 | 4 - 21 files changed, 9 insertions(+), 445 deletions(-) delete mode 100644 jenkins/Internalize packages from the Community Repository/builds/legacyIds delete mode 100644 jenkins/Internalize packages from the Community Repository/builds/permalinks delete mode 100644 jenkins/Internalize packages from the Community Repository/config.xml delete mode 100644 jenkins/Internalize packages from the Community Repository/nextBuildNumber delete mode 100644 jenkins/Update production repository/builds/legacyIds delete mode 100644 jenkins/Update production repository/builds/permalinks delete mode 100644 jenkins/Update production repository/config.xml delete mode 100644 jenkins/Update production repository/nextBuildNumber delete mode 100644 jenkins/Update test repository from Chocolatey Community Repository/builds/legacyIds delete mode 100644 jenkins/Update test repository from Chocolatey Community Repository/builds/permalinks delete mode 100644 jenkins/Update test repository from Chocolatey Community Repository/config.xml delete mode 100644 jenkins/Update test repository from Chocolatey Community Repository/nextBuildNumber delete mode 100644 jenkins/scripts/ConvertTo-ChocoObject.ps1 delete mode 100644 jenkins/scripts/Get-UpdatedPackage.ps1 delete mode 100644 jenkins/scripts/Invoke-ChocolateyInternalizer.ps1 delete mode 100644 jenkins/scripts/Update-ProdRepoFromTest.ps1 diff --git a/Set-SslSecurity.ps1 b/Set-SslSecurity.ps1 index 007c019..97456dd 100644 --- a/Set-SslSecurity.ps1 +++ b/Set-SslSecurity.ps1 @@ -194,10 +194,7 @@ process { <# Jenkins #> $JenkinsHome = "C:\ProgramData\Jenkins\.jenkins" - # Update Jenkins Jobs with Nexus URL - Get-ChildItem -Path "$JenkinsHome\jobs" -Recurse -File -Filter 'config.xml' | Invoke-TextReplacementInFile -Replacement @{ - '(?<=https:\/\/)(?.+)(?=:8443\/repository\/)' = $SubjectWithoutCn - } + Set-JenkinsLocationConfiguration -Url "https://$($SubjectWithoutCn):7443" -Path $JenkinsHome\jenkins.model.JenkinsLocationConfiguration.xml # Generate Jenkins keystore Set-JenkinsCertificate -Thumbprint $Certificate.Thumbprint diff --git a/Start-C4bJenkinsSetup.ps1 b/Start-C4bJenkinsSetup.ps1 index 0a52c4c..2806de2 100644 --- a/Start-C4bJenkinsSetup.ps1 +++ b/Start-C4bJenkinsSetup.ps1 @@ -54,17 +54,9 @@ process { $JenkinsCred = Set-JenkinsPassword -UserName 'admin' -NewPassword $(New-ServicePassword) -PassThru #endregion - # Long winded way to get the scripts for Jenkins jobs into the right place, but easier to maintain going forward - $root = Split-Path -Parent $MyInvocation.MyCommand.Definition - $systemRoot = $env:SystemDrive + '\' - $JenkinsRoot = Join-Path $root -ChildPath 'jenkins' - $jenkinsScripts = Join-Path $JenkinsRoot -ChildPath 'scripts' - - #Set home directory of Jenkins install + # Set home directory of Jenkins install $JenkinsHome = 'C:\ProgramData\Jenkins\.jenkins' - Copy-Item $jenkinsScripts $systemRoot -Recurse -Force - Stop-Service -Name Jenkins #region Jenkins Plugin Install & Update @@ -103,13 +95,8 @@ process { #endregion #region Job Config - Write-Host "Creating Chocolatey Jobs" -ForegroundColor Green - Get-ChildItem "$env:SystemDrive\choco-setup\files\jenkins" | Copy-Item -Destination "$JenkinsHome\jobs\" -Recurse -Force - - Get-ChildItem -Path "$JenkinsHome\jobs" -Recurse -File -Filter 'config.xml' | Invoke-TextReplacementInFile -Replacement @{ - '{{NugetApiKey}}' = $NuGetApiKey - '(?<=https:\/\/)(?.+)(?=:8443\/repository\/)' = $HostName - } + $chocoArgs = @('install', 'chocolatey-licensed-jenkins-jobs', "--source='ChocolateyInternal'", '-y', '--no-progress') + & Invoke-Choco @chocoArgs #endregion Write-Host "Starting Jenkins service back up" -ForegroundColor Green diff --git a/files/chocolatey.json b/files/chocolatey.json index ebf4f14..6479978 100644 --- a/files/chocolatey.json +++ b/files/chocolatey.json @@ -4,6 +4,8 @@ { "name": "chocolatey-compatibility.extension" }, { "name": "chocolatey-core.extension" }, { "name": "chocolatey-dotnetfx.extension" }, + { "name": "chocolatey-licensed-jenkins-jobs" }, + { "name": "chocolatey-licensed-jenkins-scripts" }, { "name": "chocolatey-management-database", "internalize": false }, { "name": "chocolatey-management-service", "internalize": false }, { "name": "chocolatey-management-web", "internalize": false }, diff --git a/jenkins/Internalize packages from the Community Repository/builds/legacyIds b/jenkins/Internalize packages from the Community Repository/builds/legacyIds deleted file mode 100644 index e69de29..0000000 diff --git a/jenkins/Internalize packages from the Community Repository/builds/permalinks b/jenkins/Internalize packages from the Community Repository/builds/permalinks deleted file mode 100644 index d7703c1..0000000 --- a/jenkins/Internalize packages from the Community Repository/builds/permalinks +++ /dev/null @@ -1,2 +0,0 @@ -lastFailedBuild -1 -lastSuccessfulBuild -1 diff --git a/jenkins/Internalize packages from the Community Repository/config.xml b/jenkins/Internalize packages from the Community Repository/config.xml deleted file mode 100644 index c74691e..0000000 --- a/jenkins/Internalize packages from the Community Repository/config.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - Add new packages for internalizing from the Community Repository. - false - - - - - - P_PKG_LIST - List of Chocolatey packages to be internalized (comma separated). - - true - - - P_DST_URL - Internal package repository URL. - https://{{hostname}}:8443/repository/ChocolateyTest/index.json - true - - - P_API_KEY - API key for the internal test repository - {{NugetApiKey}} - - - - - - - true - - - false - diff --git a/jenkins/Internalize packages from the Community Repository/nextBuildNumber b/jenkins/Internalize packages from the Community Repository/nextBuildNumber deleted file mode 100644 index d00491f..0000000 --- a/jenkins/Internalize packages from the Community Repository/nextBuildNumber +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/jenkins/Update production repository/builds/legacyIds b/jenkins/Update production repository/builds/legacyIds deleted file mode 100644 index e69de29..0000000 diff --git a/jenkins/Update production repository/builds/permalinks b/jenkins/Update production repository/builds/permalinks deleted file mode 100644 index 3e5edba..0000000 --- a/jenkins/Update production repository/builds/permalinks +++ /dev/null @@ -1,6 +0,0 @@ -lastCompletedBuild -1 -lastFailedBuild -1 -lastStableBuild -1 -lastSuccessfulBuild -1 -lastUnstableBuild -1 -lastUnsuccessfulBuild -1 diff --git a/jenkins/Update production repository/config.xml b/jenkins/Update production repository/config.xml deleted file mode 100644 index a9d0f31..0000000 --- a/jenkins/Update production repository/config.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - Determine new packages on the Test repository, test and submit them to the Production repository. - false - - - - - - P_PROD_REPO_URL - URL to the production repository - https://{{hostname}}:8443/repository/ChocolateyInternal/index.json - true - - - P_PROD_REPO_API_KEY - API key for the production repository. - {{NugetApiKey}} - - - P_TEST_REPO_URL - URL for the test repository. - https://{{hostname}}:8443/repository/ChocolateyTest/index.json - true - - - - - - - - Internalize packages from the Community Repository, Update test repository from Chocolatey Community Repository, - - SUCCESS - 0 - BLUE - true - - - - - - - - true - - - false - diff --git a/jenkins/Update production repository/nextBuildNumber b/jenkins/Update production repository/nextBuildNumber deleted file mode 100644 index d00491f..0000000 --- a/jenkins/Update production repository/nextBuildNumber +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/jenkins/Update test repository from Chocolatey Community Repository/builds/legacyIds b/jenkins/Update test repository from Chocolatey Community Repository/builds/legacyIds deleted file mode 100644 index e69de29..0000000 diff --git a/jenkins/Update test repository from Chocolatey Community Repository/builds/permalinks b/jenkins/Update test repository from Chocolatey Community Repository/builds/permalinks deleted file mode 100644 index d7703c1..0000000 --- a/jenkins/Update test repository from Chocolatey Community Repository/builds/permalinks +++ /dev/null @@ -1,2 +0,0 @@ -lastFailedBuild -1 -lastSuccessfulBuild -1 diff --git a/jenkins/Update test repository from Chocolatey Community Repository/config.xml b/jenkins/Update test repository from Chocolatey Community Repository/config.xml deleted file mode 100644 index 6eeae59..0000000 --- a/jenkins/Update test repository from Chocolatey Community Repository/config.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - Automatically update any out of date packages in the test repository from the Community Repository - false - - - - - - P_LOCAL_REPO_URL - Internal test repository. - https://{{hostname}}:8443/repository/ChocolateyTest/index.json - true - - - P_REMOTE_REPO_URL - Remote repository containing updated package versions. - https://community.chocolatey.org/api/v2/ - true - - - P_LOCAL_REPO_API_KEY - API key for the internal test repository where updated packages will be pushed. - {{NugetApiKey}} - - - - - - - true - - - false - diff --git a/jenkins/Update test repository from Chocolatey Community Repository/nextBuildNumber b/jenkins/Update test repository from Chocolatey Community Repository/nextBuildNumber deleted file mode 100644 index d00491f..0000000 --- a/jenkins/Update test repository from Chocolatey Community Repository/nextBuildNumber +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/jenkins/scripts/ConvertTo-ChocoObject.ps1 b/jenkins/scripts/ConvertTo-ChocoObject.ps1 deleted file mode 100644 index 7702d9c..0000000 --- a/jenkins/scripts/ConvertTo-ChocoObject.ps1 +++ /dev/null @@ -1,16 +0,0 @@ -function ConvertTo-ChocoObject { - [CmdletBinding()] - param ( - [Parameter(Mandatory, ValueFromPipeline)] - [string] - $InputObject - ) - process { - # format of the 'choco list -r' output is: - # | (ie. adobereader|2015.6.7) - if (-not [string]::IsNullOrEmpty($InputObject)) { - $props = $_.split('|') - New-Object -TypeName psobject -Property @{ Name = $props[0]; Version = $props[1] } - } - } -} diff --git a/jenkins/scripts/Get-UpdatedPackage.ps1 b/jenkins/scripts/Get-UpdatedPackage.ps1 deleted file mode 100644 index 0e2aa01..0000000 --- a/jenkins/scripts/Get-UpdatedPackage.ps1 +++ /dev/null @@ -1,68 +0,0 @@ -[CmdletBinding()] -Param ( - [Parameter(Mandatory)] - [string] - $LocalRepo, - - [Parameter(Mandatory)] - [string] - $LocalRepoApiKey, - - [Parameter(Mandatory)] - [string] - $RemoteRepo -) - -. "$PSScriptRoot\ConvertTo-ChocoObject.ps1" - -if (([version] (choco --version).Split('-')[0]) -ge [version] '2.1.0') { - Write-Verbose "Clearing Chocolatey CLI cache to ensure latest package information is retrieved." - choco cache remove -} - -$LocalRepoSource = $(choco source --limit-output | ConvertFrom-Csv -Delimiter '|' -Header Name, Uri, Disabled).Where{ - $_.Uri -eq $LocalRepo -or - $_.Name -eq $LocalRepo -}[0] - -Write-Verbose "Getting list of local packages from '$LocalRepo'." -$localPkgs = choco search --source $LocalRepo -r | ConvertTo-ChocoObject -Write-Verbose "Retrieved list of $(($localPkgs).count) packages from '$Localrepo'." - -$localPkgs | ForEach-Object { - Write-Verbose "Getting remote package information for '$($_.name)'." - $remotePkg = choco search $_.name --source $RemoteRepo --exact -r | ConvertTo-ChocoObject - if ([version]($remotePkg.version) -gt ([version]$_.version)) { - Write-Verbose "Package '$($_.name)' has a remote version of '$($remotePkg.version)' which is later than the local version '$($_.version)'." - Write-Verbose "Internalizing package '$($_.name)' with version '$($remotePkg.version)'." - $tempPath = Join-Path -Path $env:TEMP -ChildPath ([GUID]::NewGuid()).GUID - choco download $_.name --no-progress --internalize --force --internalize-all-urls --append-use-original-location --output-directory=$tempPath --source=$RemoteRepo - - if ($LASTEXITCODE -eq 0) { - try { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source enable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} - - Write-Verbose "Pushing package '$($_.name)' to local repository '$LocalRepo'." - (Get-Item -Path (Join-Path -Path $tempPath -ChildPath "*.nupkg")).fullname | ForEach-Object { - choco push $_ --source $LocalRepo --api-key $LocalRepoApiKey --force - if ($LASTEXITCODE -eq 0) { - Write-Verbose "Package '$_' pushed to '$LocalRepo'." - } - else { - Write-Verbose "Package '$_' could not be pushed to '$LocalRepo'.`nThis could be because it already exists in the repository at a higher version and can be mostly ignored. Check error logs." - } - } - } finally { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source disable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} - } - } - else { - Write-Verbose "Failed to download package '$($_.name)'" - } - Remove-Item $tempPath -Recurse -Force - } - - else { - Write-Verbose "Package '$($_.name)' has a remote version of '$($remotePkg.version)' which is not later than the local version '$($_.version)'." - } -} diff --git a/jenkins/scripts/Invoke-ChocolateyInternalizer.ps1 b/jenkins/scripts/Invoke-ChocolateyInternalizer.ps1 deleted file mode 100644 index f2d5561..0000000 --- a/jenkins/scripts/Invoke-ChocolateyInternalizer.ps1 +++ /dev/null @@ -1,68 +0,0 @@ -<# - .SYNOPSIS - Internalizes packages from the community repository to an internal location. - - .DESCRIPTION - Internalizes packages from a specified repository (the Chocolatey Community - repository by default) into the target internal repository. All download - URLs and necessary resources are internalized to create a self-contained - package. - - .EXAMPLE - ./Internalizer.ps1 -Package googlechrome -RepositoryUrl https://chocoserver:8443/repository/ChocolateyInternal/index.json -LocalRepoApiKey 61332b06-d849-476c-b2ab-a290372c17d7 -#> -[CmdletBinding()] -param( - # The package(s) you want to internalize (comma separated). - [Parameter(Mandatory, Position = 0, ValueFromPipeline)] - [string[]] - $Package, - - # Your internal nuget repository url to push packages to. - [Parameter(Mandatory, Position = 1)] - [string] - $RepositoryUrl, - - # The API key of your internal repository server. - [Parameter(Mandatory, Position = 3)] - [string] - $NexusApiKey, - - # The remote repo to check against. Defaults to https://community.chocolatey.org/api/v2 - [Parameter(Position = 2)] - [string] - $RemoteRepo = 'https://community.chocolatey.org/api/v2/' -) -begin { - if (!(Test-Path "$env:ChocolateyInstall\license")) { - throw "Licensed edition required to use Package Internalizer" - } - - $Guid = [Guid]::NewGuid().Guid - $TempFolder = [IO.Path]::GetTempPath() | - Join-Path -ChildPath $Guid | - New-Item -ItemType Directory -Path { $_ } | - Select-Object -ExpandProperty FullName - - $LocalRepoSource = $(choco source --limit-output | ConvertFrom-Csv -Delimiter '|' -Header Name, Uri, Disabled).Where{ - $_.Uri -eq $RepositoryUrl - }[0] -} -process { - foreach ($item in $Package) { - choco download $item --internalize --output-directory="'$TempFolder'" --no-progress --internalize-all-urls --append-use-original-location --source="'$RemoteRepo'" - try { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source enable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} - - Get-ChildItem -Path $TempFolder -Filter *.nupkg -Recurse -File | ForEach-Object { - choco push $_.Fullname --source="'$RepositoryUrl'" --api-key="'$NexusApiKey'" --force - Remove-Item -Path $_.FullName -Force - } - } finally { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source disable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} - } - } -} -end { - Remove-Item -Path $TempFolder -Recurse -Force -} diff --git a/jenkins/scripts/Update-ProdRepoFromTest.ps1 b/jenkins/scripts/Update-ProdRepoFromTest.ps1 deleted file mode 100644 index 65a0e1f..0000000 --- a/jenkins/scripts/Update-ProdRepoFromTest.ps1 +++ /dev/null @@ -1,81 +0,0 @@ -[CmdletBinding()] -param ( - [Parameter(Mandatory)] - [string] - $ProdRepo, - - [Parameter(Mandatory)] - [string] - $ProdRepoApiKey, - - [Parameter(Mandatory)] - [string] - $TestRepo -) - -if (([version] (choco --version).Split('-')[0]) -ge [version] '2.1.0') { - Write-Verbose "Clearing Chocolatey CLI cache to ensure latest package information is retrieved." - choco cache remove -} - -$LocalRepoSource = $(choco source --limit-output | ConvertFrom-Csv -Delimiter '|' -Header Name, Uri, Disabled).Where{ - $_.Uri -eq $TestRepo -or - $_.Name -eq $TestRepo -}[0] - -Write-Verbose "Checking the list of packages available in the test and prod repositories" -try { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source enable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} - $testPkgs = choco search --source $TestRepo --all-versions --limit-output | ConvertFrom-Csv -Delimiter '|' -Header Name, Version -} finally { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source disable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} -} -$prodPkgs = choco search --source $ProdRepo --all-versions --limit-output | ConvertFrom-Csv -Delimiter '|' -Header Name, Version -$tempPath = Join-Path -Path $env:TEMP -ChildPath ([GUID]::NewGuid()).GUID - -$Packages = if ($null -eq $testPkgs) { - Write-Verbose "Test repository appears to be empty. Nothing to push to production." -} -elseif ($null -eq $prodPkgs) { - $testPkgs -} -else { - Compare-Object -ReferenceObject $testpkgs -DifferenceObject $prodpkgs -Property name, version | Where-Object SideIndicator -EQ '<=' -} - -$Packages | ForEach-Object { - Write-Verbose "Downloading package '$($_.Name)' v$($_.Version) to '$tempPath'." - try { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source enable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} - choco download $_.Name --version $_.Version --no-progress --output-directory=$tempPath --source=$TestRepo --ignore-dependencies - } finally { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source disable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} - } - - if ($LASTEXITCODE -eq 0) { - $pkgPath = (Get-Item -Path (Join-Path -Path $tempPath -ChildPath '*.nupkg')).FullName - # ####################### - # INSERT CODE HERE TO TEST YOUR PACKAGE - # ####################### - - # If package testing is successful ... - if ($LASTEXITCODE -eq 0) { - Write-Verbose "Pushing downloaded package '$(Split-Path -Path $pkgPath -Leaf)' to production repository '$ProdRepo'." - choco push $pkgPath --source=$ProdRepo --api-key=$ProdRepoApiKey --force - - if ($LASTEXITCODE -eq 0) { - Write-Verbose "Pushed package successfully." - } - else { - Write-Verbose "Could not push package." - } - } - else { - Write-Verbose "Package testing failed." - } - Remove-Item -Path $tempPath -Recurse -Force - } - else { - Write-Verbose "Could not download package." - } -} diff --git a/modules/C4B-Environment/C4B-Environment.psm1 b/modules/C4B-Environment/C4B-Environment.psm1 index dbbfee6..3a128a2 100644 --- a/modules/C4B-Environment/C4B-Environment.psm1 +++ b/modules/C4B-Environment/C4B-Environment.psm1 @@ -2094,7 +2094,9 @@ function Get-ChocoEnvironmentProperty { [switch]$AsPlainText ) begin { - $Content = Import-Clixml -Path "$env:SystemDrive\choco-setup\clixml\chocolatey-for-business.xml" + if (Test-Path "$env:SystemDrive\choco-setup\clixml\chocolatey-for-business.xml") { + $Content = Import-Clixml -Path "$env:SystemDrive\choco-setup\clixml\chocolatey-for-business.xml" + } } process { if ($Name) { diff --git a/tests/jenkins.tests.ps1 b/tests/jenkins.tests.ps1 index 5b0a13b..acea70e 100644 --- a/tests/jenkins.tests.ps1 +++ b/tests/jenkins.tests.ps1 @@ -31,10 +31,6 @@ Describe "Jenkins Configuration" { $Scripts = (Get-ChildItem 'C:\Scripts' -Recurse -Filter *.ps1).Name } - It "ConvertTo-ChocoObject is present" { - 'ConvertTo-ChocoObject.ps1' -in $Scripts | Should -Be $true - } - It "Get-UpdatedPackage.ps1 is present" { 'Get-UpdatedPackage.ps1' -in $Scripts | Should -Be $true } From b9a5b0c5b2cb42b9eb4a824f6edd72ec464edcd4 Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Tue, 18 Feb 2025 12:37:02 +0000 Subject: [PATCH 02/17] (#245) Uses NexuShell for Nexus Operations Additionally, hardens some flakey Nexus operations to improve reliability. --- Set-SslSecurity.ps1 | 62 +- Start-C4bNexusSetup.ps1 | 5 +- files/chocolatey.json | 1 + modules/C4B-Environment/C4B-Environment.psd1 | 4 +- modules/C4B-Environment/C4B-Environment.psm1 | 1365 +----------------- tests/server.tests.ps1 | 4 +- 6 files changed, 65 insertions(+), 1376 deletions(-) diff --git a/Set-SslSecurity.ps1 b/Set-SslSecurity.ps1 index 97456dd..bc3237f 100644 --- a/Set-SslSecurity.ps1 +++ b/Set-SslSecurity.ps1 @@ -65,13 +65,12 @@ param( process { $DefaultEap = $ErrorActionPreference $ErrorActionPreference = 'Stop' - Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Set-SslCertificate-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" + Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Set-SslSecurity-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" # Collect current certificate configuration $Certificate = if ($Subject) { Get-Certificate -Subject $Subject - } - elseif ($Thumbprint) { + } elseif ($Thumbprint) { Get-Certificate -Thumbprint $Thumbprint } @@ -84,12 +83,10 @@ process { $CertificateDnsName = Read-Host -Prompt "$(if ($CertificateDnsName) {"'$($CertificateDnsName)' is not a subdomain of '$($Matches.Subject)'. "})Please provide an FQDN to use with the certificate '$($Matches.Subject)'" } $CertificateDnsName - } - else { + } else { $Matches.Subject } - } - else { + } else { $SubjectWithoutCn = $CertificateDnsName } @@ -105,7 +102,7 @@ process { # Add firewall rule for Nexus netsh advfirewall firewall add rule name="Nexus-8443" dir=in action=allow protocol=tcp localport=8443 - + Write-Verbose "Starting up Nexus" Start-Service nexus @@ -116,14 +113,18 @@ process { Invoke-WebRequest "https://${SubjectWithoutCn}:8443" -UseBasicParsing -ErrorAction Stop Start-Sleep -Seconds 3 } catch {} - } until($response.StatusCode -eq '200') + } until ($response.StatusCode -eq '200') Write-Host "Nexus is ready!" Invoke-Choco source remove --name="'ChocolateyInternal'" # Build Credential Object, Connect to Nexus - $securePw = (Get-Content 'C:\programdata\sonatype-work\nexus3\admin.password') | ConvertTo-SecureString -AsPlainText -Force - $Credential = [System.Management.Automation.PSCredential]::new('admin', $securePw) + if ($Credential = Get-ChocoEnvironmentProperty NexusCredential) { + Write-Verbose "Using stored Nexus Credential" + } elseif (Test-Path 'C:\programdata\sonatype-work\nexus3\admin.password') { + $securePw = (Get-Content 'C:\programdata\sonatype-work\nexus3\admin.password') | ConvertTo-SecureString -AsPlainText -Force + $Credential = [System.Management.Automation.PSCredential]::new('admin', $securePw) + } # Connect to Nexus Connect-NexusServer -Hostname $SubjectWithoutCn -Credential $Credential -UseSSL @@ -131,10 +132,10 @@ process { # Push ClientSetup.ps1 to raw repo $ClientScript = "$PSScriptRoot\scripts\ClientSetup.ps1" (Get-Content -Path $ClientScript) -replace "{{hostname}}", $SubjectWithoutCn | Set-Content -Path $ClientScript - New-NexusRawComponent -RepositoryName 'choco-install' -File $ClientScript + $null = New-NexusRawComponent -RepositoryName 'choco-install' -File $ClientScript # Disable anonymous authentication - Set-NexusAnonymousAuth -Disabled + $null = Set-NexusAnonymousAuth -Disabled if (-not (Get-NexusRole -Role 'chocorole' -ErrorAction SilentlyContinue)) { # Create Nexus role @@ -144,7 +145,12 @@ process { Description = "Role for web enabled choco clients" Privileges = @('nx-repository-view-nuget-*-browse', 'nx-repository-view-nuget-*-read', 'nx-repository-view-raw-*-read', 'nx-repository-view-raw-*-browse') } - New-NexusRole @RoleParams + $null = New-NexusRole @RoleParams + + $Timeout = [System.Diagnostics.Stopwatch]::StartNew() + while ($Timeout.Elapsed.TotalSeconds -lt 30 -and -not (Get-NexusRole -Role $RoleParams.Id -ErrorAction SilentlyContinue)) { + Start-Sleep -Seconds 3 + } } if (-not (Get-NexusUser -User 'chocouser' -ErrorAction SilentlyContinue)) { @@ -159,7 +165,14 @@ process { Status = 'Active' Roles = 'chocorole' } - New-NexusUser @UserParams + $null = New-NexusUser @UserParams + + $Timeout = [System.Diagnostics.Stopwatch]::StartNew() + while ($Timeout.Elapsed.TotalSeconds -lt 30 -and -not (Get-NexusUser -User $UserParams.Username -ErrorAction SilentlyContinue)) { + Start-Sleep -Seconds 3 + } + } else { + $NexusPw = Get-ChocoEnvironmentProperty ChocoUserPassword } # Update all sources with credentials and the new path @@ -209,7 +222,7 @@ process { <# CCM #> # Update the service certificate Set-CcmCertificate -CertificateThumbprint $Certificate.Thumbprint - + # Remove old CCM web binding, and add new CCM web binding Stop-CcmService Remove-CcmBinding @@ -226,15 +239,24 @@ process { # Generate Register-C4bEndpoint.ps1 $EndpointScript = "$PSScriptRoot\scripts\Register-C4bEndpoint.ps1" - $ClientSaltValue = New-CCMSalt - $ServiceSaltValue = New-CCMSalt + if ($ClientSaltValue = Get-ChocoEnvironmentProperty ClientSalt -AsPlainText) { + Write-Verbose "Using stored client salt value." + } else { + $ClientSaltValue = New-CCMSalt + } + + if ($ServiceSaltValue = Get-ChocoEnvironmentProperty ClientSalt -AsPlainText) { + Write-Verbose "Using stored service salt value." + } else { + $ServiceSaltValue = New-CCMSalt + } Invoke-TextReplacementInFile -Path $EndpointScript -Replacement @{ "{{ ClientSaltValue }}" = $ClientSaltValue - "{{ ServiceSaltValue }}" = $ServiceSaltValue + "{{ ServiceSaltValue }}" = $ServiceSaltValue "{{ FQDN }}" = $SubjectWithoutCn } - + # Agent Setup $agentArgs = @{ CentralManagementServiceUrl = "https://$($SubjectWithoutCn):24020/ChocolateyManagementService" diff --git a/Start-C4bNexusSetup.ps1 b/Start-C4bNexusSetup.ps1 index a8249d0..bba8ead 100644 --- a/Start-C4bNexusSetup.ps1 +++ b/Start-C4bNexusSetup.ps1 @@ -33,6 +33,9 @@ process { $chocoArgs = @('install', 'nexus-repository', '-y' ,'--no-progress', "--package-parameters='/Fqdn:localhost'") & Invoke-Choco @chocoArgs + $chocoArgs = @('install', 'nexushell', '-y' ,'--no-progress') + & Invoke-Choco @chocoArgs + #Build Credential Object, Connect to Nexus Write-Host "Configuring Sonatype Nexus Repository" $securePw = (Get-Content 'C:\programdata\sonatype-work\nexus3\admin.password') | ConvertTo-SecureString -AsPlainText -Force @@ -73,7 +76,7 @@ process { $Signature = Get-AuthenticodeSignature -FilePath $ChocoInstallScript if ($Signature.Status -eq 'Valid' -and $Signature.SignerCertificate.Subject -eq 'CN="Chocolatey Software, Inc", O="Chocolatey Software, Inc", L=Topeka, S=Kansas, C=US') { - New-NexusRawComponent -RepositoryName 'choco-install' -File $ChocoInstallScript + $null = New-NexusRawComponent -RepositoryName 'choco-install' -File $ChocoInstallScript } else { Write-Error "ChocolateyInstall.ps1 script signature is not valid. Please investigate." } diff --git a/files/chocolatey.json b/files/chocolatey.json index 6479978..4a5eb06 100644 --- a/files/chocolatey.json +++ b/files/chocolatey.json @@ -25,6 +25,7 @@ { "name": "KB3033929", "internalize": false }, { "name": "KB3035131", "internalize": false }, { "name": "microsoft-edge" }, + { "name": "nexushell", "version": "1.2.0" }, { "name": "nexus-repository" }, { "name": "pester", "internalize": false }, { "name": "sql-server-express" }, diff --git a/modules/C4B-Environment/C4B-Environment.psd1 b/modules/C4B-Environment/C4B-Environment.psd1 index 3c1a18b..d601de1 100644 --- a/modules/C4B-Environment/C4B-Environment.psd1 +++ b/modules/C4B-Environment/C4B-Environment.psd1 @@ -116,7 +116,9 @@ PrivateData = @{ # RequireLicenseAcceptance = $false # External dependent modules of this module - # ExternalModuleDependencies = @() + ExternalModuleDependencies = @( + "NexuShell" + ) } # End of PSData hashtable diff --git a/modules/C4B-Environment/C4B-Environment.psm1 b/modules/C4B-Environment/C4B-Environment.psm1 index 3a128a2..72e0654 100644 --- a/modules/C4B-Environment/C4B-Environment.psm1 +++ b/modules/C4B-Environment/C4B-Environment.psm1 @@ -193,21 +193,16 @@ function Get-ChocolateyPackageMetadata { #region Nexus functions (Start-C4BNexusSetup.ps1) function Wait-Nexus { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::tls12 - Do { + $NexusUrl = & (Get-Module NexuShell) {Get-NexusUri} + do { $response = try { - Invoke-WebRequest $("http://localhost:8081") -ErrorAction Stop - } - catch { - $null - } - - } until($response.StatusCode -eq '200') + Invoke-WebRequest $NexusUrl -ErrorAction Stop + } catch {} + } until ($response.StatusCode -eq '200') Write-Host "Nexus is ready!" - } function Invoke-NexusScript { - [CmdletBinding()] Param ( [Parameter(Mandatory)] @@ -222,1346 +217,12 @@ function Invoke-NexusScript { [String] $Script ) - - $scriptName = [GUID]::NewGuid().ToString() - $body = @{ - name = $scriptName - type = 'groovy' - content = $Script - } - - # Call the API - $baseUri = "$ServerUri/service/rest/v1/script" - - #Store the Script - $uri = $baseUri - Invoke-RestMethod -Uri $uri -ContentType 'application/json' -Body $($body | ConvertTo-Json) -Header $ApiHeader -Method Post - #Run the script - $uri = "{0}/{1}/run" -f $baseUri, $scriptName - $result = Invoke-RestMethod -Uri $uri -ContentType 'text/plain' -Header $ApiHeader -Method Post - #Delete the Script - $uri = "{0}/{1}" -f $baseUri, $scriptName - Invoke-RestMethod -Uri $uri -Header $ApiHeader -Method Delete -UseBasicParsing - - $result - -} - -function Connect-NexusServer { - <# - .SYNOPSIS - Creates the authentication header needed for REST calls to your Nexus server - - .DESCRIPTION - Creates the authentication header needed for REST calls to your Nexus server - - .PARAMETER Hostname - The hostname or ip address of your Nexus server - - .PARAMETER Credential - The credentials to authenticate to your Nexus server - - .PARAMETER UseSSL - Use https instead of http for REST calls. Defaults to 8443. - - .PARAMETER Sslport - If not the default 8443 provide the current SSL port your Nexus server uses - - .EXAMPLE - Connect-NexusServer -Hostname nexus.fabrikam.com -Credential (Get-Credential) - .EXAMPLE - Connect-NexusServer -Hostname nexus.fabrikam.com -Credential (Get-Credential) -UseSSL - .EXAMPLE - Connect-NexusServer -Hostname nexus.fabrikam.com -Credential $Cred -UseSSL -Sslport 443 - #> - [cmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/Connect-NexusServer/')] - param( - [Parameter(Mandatory, Position = 0)] - [Alias('Server')] - [String] - $Hostname, - - [Parameter(Mandatory, Position = 1)] - [System.Management.Automation.PSCredential] - $Credential, - - [Parameter()] - [Switch] - $UseSSL, - - [Parameter()] - [String] - $Sslport = '8443' - ) - - process { - - if ($UseSSL) { - $script:protocol = 'https' - $script:port = $Sslport - } - else { - $script:protocol = 'http' - $script:port = '8081' - } - - $script:HostName = $Hostname - - $credPair = "{0}:{1}" -f $Credential.UserName, $Credential.GetNetworkCredential().Password - - $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($credPair)) - - $script:header = @{ Authorization = "Basic $encodedCreds" } - - try { - $url = "$($protocol)://$($Hostname):$($port)/service/rest/v1/status" - - $params = @{ - Headers = $header - ContentType = 'application/json' - Method = 'GET' - Uri = $url - } - - $null = Invoke-RestMethod @params -ErrorAction Stop - Write-Host "Connected to $Hostname" -ForegroundColor Green - } - - catch { - $_.Exception.Message - } - } -} - -function Invoke-Nexus { - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [String] - $UriSlug, - - [Parameter()] - [Hashtable] - $Body, - - [Parameter()] - [Array] - $BodyAsArray, - - [Parameter()] - [String] - $BodyAsString, - - [Parameter()] - [String] - $File, - - [Parameter()] - [String] - $ContentType = 'application/json', - - [Parameter(Mandatory)] - [String] - $Method, - - [hashtable] - $AdditionalHeaders = @{} - ) - process { - $UriBase = "$($protocol)://$($Hostname):$($port)" - $Uri = $UriBase + $UriSlug - $Params = @{ - Headers = $header + $AdditionalHeaders - ContentType = $ContentType - Uri = $Uri - Method = $Method - } - - if ($Body) { - $Params.Add('Body', $($Body | ConvertTo-Json -Depth 3)) - } - - if ($BodyAsArray) { - $Params.Add('Body', $($BodyAsArray | ConvertTo-Json -Depth 3)) - } - - if ($BodyAsString) { - $Params.Add('Body', $BodyAsString) - } - - if ($File) { - $Params.Remove('ContentType') - $Params.Add('InFile', $File) - } - - Invoke-RestMethod @Params - - - } -} - -function Get-NexusUserToken { - <# - .SYNOPSIS - Fetches a User Token for the provided credential - - .DESCRIPTION - Fetches a User Token for the provided credential - - .PARAMETER Credential - The Nexus user for which to receive a token - - .NOTES - This is a private function not exposed to the end user. - #> - [CmdletBinding()] - Param( - [Parameter(Mandatory)] - [PSCredential] - $Credential - ) - - process { - $UriBase = "$($protocol)://$($Hostname):$($port)" - - $slug = '/service/extdirect' - - $uri = $UriBase + $slug - - $data = @{ - action = 'rapture_Security' - method = 'authenticationToken' - data = @("$([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($($Credential.Username))))", "$([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($($Credential.GetNetworkCredential().Password))))") - type = 'rpc' - tid = 16 - } - - Write-Verbose ($data | ConvertTo-Json) - $result = Invoke-RestMethod -Uri $uri -Method POST -Body ($data | ConvertTo-Json) -ContentType 'application/json' -Headers $header - $token = $result.result.data - $token - } - -} - -function Get-NexusRepository { - <# - .SYNOPSIS - Returns info about configured Nexus repository - - .DESCRIPTION - Returns details for currently configured repositories on your Nexus server - - .PARAMETER Format - Query for only a specific repository format. E.g. nuget, maven2, or docker - - .PARAMETER Name - Query for a specific repository by name - - .EXAMPLE - Get-NexusRepository - .EXAMPLE - Get-NexusRepository -Format nuget - .EXAMPLE - Get-NexusRepository -Name CompanyNugetPkgs - #> - [cmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/Get-NexusRepository/', DefaultParameterSetName = "default")] - param( - [Parameter(ParameterSetName = "Format", Mandatory)] - [String] - [ValidateSet('apt', 'bower', 'cocoapods', 'conan', 'conda', 'docker', 'gitlfs', 'go', 'helm', 'maven2', 'npm', 'nuget', 'p2', 'pypi', 'r', 'raw', 'rubygems', 'yum')] - $Format, - - [Parameter(ParameterSetName = "Type", Mandatory)] - [String] - [ValidateSet('hosted', 'group', 'proxy')] - $Type, - - [Parameter(ParameterSetName = "Name", Mandatory)] - [String] - $Name - ) - - - begin { - - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - $urislug = "/service/rest/v1/repositories" - } - process { - - switch ($PSCmdlet.ParameterSetName) { - { $Format } { - $filter = { $_.format -eq $Format } - - $result = Invoke-Nexus -UriSlug $urislug -Method Get - $result | Where-Object $filter - - } - - { $Name } { - $filter = { $_.name -eq $Name } - - $result = Invoke-Nexus -UriSlug $urislug -Method Get - $result | Where-Object $filter - - } - - { $Type } { - $filter = { $_.type -eq $Type } - $result = Invoke-Nexus -UriSlug $urislug -Method Get - $result | Where-Object $filter - } - - default { - Invoke-Nexus -UriSlug $urislug -Method Get | ForEach-Object { - [pscustomobject]@{ - Name = $_.SyncRoot.name - Format = $_.SyncRoot.format - Type = $_.SyncRoot.type - Url = $_.SyncRoot.url - Attributes = $_.SyncRoot.attributes - } - } - } - } - } -} - -function Remove-NexusRepository { - <# - .SYNOPSIS - Removes a given repository from the Nexus instance - - .DESCRIPTION - Removes a given repository from the Nexus instance - - .PARAMETER Repository - The repository to remove - - .PARAMETER Force - Disable prompt for confirmation before removal - - .EXAMPLE - Remove-NexusRepository -Repository ProdNuGet - .EXAMPLE - Remove-NexusRepository -Repository MavenReleases -Force() - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/Remove-NexusRepository/', SupportsShouldProcess, ConfirmImpact = 'High')] - Param( - [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] - [Alias('Name')] - [ArgumentCompleter( { - param($command, $WordToComplete, $CommandAst, $FakeBoundParams) - $repositories = (Get-NexusRepository).Name - - if ($WordToComplete) { - $repositories.Where{ $_ -match "^$WordToComplete" } - } - else { - $repositories - } - })] - [String[]] - $Repository, - - [Parameter()] - [Switch] - $Force - ) - begin { - - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - $urislug = "/service/rest/v1/repositories" - } - process { - - $Repository | Foreach-Object { - $Uri = $urislug + "/$_" - - try { - - if ($Force -and -not $Confirm) { - $ConfirmPreference = 'None' - if ($PSCmdlet.ShouldProcess("$_", "Remove Repository")) { - $result = Invoke-Nexus -UriSlug $Uri -Method 'DELETE' -ErrorAction Stop - [pscustomobject]@{ - Status = 'Success' - Repository = $_ - } - } - } - else { - if ($PSCmdlet.ShouldProcess("$_", "Remove Repository")) { - $result = Invoke-Nexus -UriSlug $Uri -Method 'DELETE' -ErrorAction Stop - [pscustomobject]@{ - Status = 'Success' - Repository = $_ - Timestamp = $result.date - } - } - } - } - - catch { - $_.exception.message - } - } - } -} - -function Remove-NexusRepositoryFolder { - <# - .SYNOPSIS - Removes a given folder from a repository from the Nexus instance - - .PARAMETER RepositoryName - The repository to remove from - - .PARAMETER Name - The name of the folder to remove - - .EXAMPLE - Remove-NexusRepositoryFolder -RepositoryName MyNuGetRepo -Name 'v3' - # Removes the v3 folder in the MyNuGetRepo repository - #> - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [string]$RepositoryName, - - [Parameter(Mandatory)] - [string]$Name - ) - end { - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - $ApiParameters = @{ - UriSlug = "/service/extdirect" - Method = "POST" - Body = @{ - action = "coreui_Component" - method = "deleteFolder" - data = @( - $Name, - $RepositoryName - ) - type = "rpc" - tid = Get-Random -Minimum 1 -Maximum 100 - } - AdditionalHeaders = @{ - "X-Nexus-UI" = "true" - } - } - - $Result = Invoke-Nexus @ApiParameters - - if (-not $Result.result.success) { - throw "Failed to delete folder: $($Result.result.message)" - } - } -} - -function New-NexusNugetHostedRepository { - <# - .SYNOPSIS - Creates a new NuGet Hosted repository - - .DESCRIPTION - Creates a new NuGet Hosted repository - - .PARAMETER Name - The name of the repository - - .PARAMETER CleanupPolicy - The Cleanup Policies to apply to the repository - - - .PARAMETER Online - Marks the repository to accept incoming requests - - .PARAMETER BlobStoreName - Blob store to use to store NuGet packages - - .PARAMETER StrictContentValidation - Validate that all content uploaded to this repository is of a MIME type appropriate for the repository format - - .PARAMETER DeploymentPolicy - Controls if deployments of and updates to artifacts are allowed - - .PARAMETER HasProprietaryComponents - Components in this repository count as proprietary for namespace conflict attacks (requires Sonatype Nexus Firewall) - - .EXAMPLE - New-NexusNugetHostedRepository -Name NugetHostedTest -DeploymentPolicy Allow - .EXAMPLE - $RepoParams = @{ - Name = MyNuGetRepo - CleanupPolicy = '90 Days' - DeploymentPolicy = 'Allow' - UseStrictContentValidation = $true - } - - New-NexusNugetHostedRepository @RepoParams - .NOTES - General notes - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/New-NexusNugetHostedRepository/')] - Param( - [Parameter(Mandatory)] - [String] - $Name, - - [Parameter()] - [String] - $CleanupPolicy, - - [Parameter()] - [Switch] - $Online = $true, - - [Parameter()] - [String] - $BlobStoreName = 'default', - - [Parameter()] - [ValidateSet('True', 'False')] - [String] - $UseStrictContentValidation = 'True', - - [Parameter()] - [ValidateSet('Allow', 'Deny', 'Allow_Once')] - [String] - $DeploymentPolicy, - - [Parameter()] - [Switch] - $HasProprietaryComponents - ) - - begin { - - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - $urislug = "/service/rest/v1/repositories" - - } - - process { - $formatUrl = $urislug + '/nuget' - - $FullUrlSlug = $formatUrl + '/hosted' - - - $body = @{ - name = $Name - online = [bool]$Online - storage = @{ - blobStoreName = $BlobStoreName - strictContentTypeValidation = $UseStrictContentValidation - writePolicy = $DeploymentPolicy - } - cleanup = @{ - policyNames = @($CleanupPolicy) - } - } - - if ($HasProprietaryComponents) { - $Prop = @{ - proprietaryComponents = 'True' - } - - $Body.Add('component', $Prop) - } - - Write-Verbose $($Body | ConvertTo-Json) - $null = Invoke-Nexus -UriSlug $FullUrlSlug -Body $Body -Method POST - - } -} - -function New-NexusRawHostedRepository { - <# - .SYNOPSIS - Creates a new Raw Hosted repository - - .DESCRIPTION - Creates a new Raw Hosted repository - - .PARAMETER Name - The Name of the repository to create - - .PARAMETER Online - Mark the repository as Online. Defaults to True - - .PARAMETER BlobStore - The blob store to attach the repository too. Defaults to 'default' - - .PARAMETER UseStrictContentTypeValidation - Validate that all content uploaded to this repository is of a MIME type appropriate for the repository format - - .PARAMETER DeploymentPolicy - Controls if deployments of and updates to artifacts are allowed - - .PARAMETER CleanupPolicy - Components that match any of the Applied policies will be deleted - - .PARAMETER HasProprietaryComponents - Components in this repository count as proprietary for namespace conflict attacks (requires Sonatype Nexus Firewall) - - .PARAMETER ContentDisposition - Add Content-Disposition header as 'Attachment' to disable some content from being inline in a browser. - - .EXAMPLE - New-NexusRawHostedRepository -Name BinaryArtifacts -ContentDisposition Attachment - .EXAMPLE - $RepoParams = @{ - Name = 'BinaryArtifacts' - Online = $true - UseStrictContentTypeValidation = $true - DeploymentPolicy = 'Allow' - CleanupPolicy = '90Days', - BlobStore = 'AmazonS3Bucket' - } - New-NexusRawHostedRepository @RepoParams - - .NOTES - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/New-NexusRawHostedRepository/', DefaultParameterSetname = "Default")] - Param( - [Parameter(Mandatory)] - [String] - $Name, - - [Parameter()] - [Switch] - $Online = $true, - - [Parameter()] - [String] - $BlobStore = 'default', - - [Parameter()] - [Switch] - $UseStrictContentTypeValidation, - - [Parameter()] - [ValidateSet('Allow', 'Deny', 'Allow_Once')] - [String] - $DeploymentPolicy = 'Allow_Once', - - [Parameter()] - [String] - $CleanupPolicy, - - [Parameter()] - [Switch] - $HasProprietaryComponents, - - [Parameter(Mandatory)] - [ValidateSet('Inline', 'Attachment')] - [String] - $ContentDisposition - ) - - begin { - - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - $urislug = "/service/rest/v1/repositories/raw/hosted" - - } - - process { - - $Body = @{ - name = $Name - online = [bool]$Online - storage = @{ - blobStoreName = $BlobStore - strictContentTypeValidation = [bool]$UseStrictContentTypeValidation - writePolicy = $DeploymentPolicy.ToLower() - } - cleanup = @{ - policyNames = @($CleanupPolicy) - } - component = @{ - proprietaryComponents = [bool]$HasProprietaryComponents - } - raw = @{ - contentDisposition = $ContentDisposition.ToUpper() - } - } - - Write-Verbose $($Body | ConvertTo-Json) - $null = Invoke-Nexus -UriSlug $urislug -Body $Body -Method POST - - - } -} - -function Get-NexusRealm { - <# - .SYNOPSIS - Gets Nexus Realm information - - .DESCRIPTION - Gets Nexus Realm information - - .PARAMETER Active - Returns only active realms - - .EXAMPLE - Get-NexusRealm - .EXAMPLE - Get-NexusRealm -Active - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/Get-NexusRealm/')] - Param( - [Parameter()] - [Switch] - $Active - ) - - begin { - - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - - $urislug = "/service/rest/v1/security/realms/available" - - - } - - process { - - if ($Active) { - $current = Invoke-Nexus -UriSlug $urislug -Method 'GET' - $urislug = '/service/rest/v1/security/realms/active' - $Activated = Invoke-Nexus -UriSlug $urislug -Method 'GET' - $current | Where-Object { $_.Id -in $Activated } - } - else { - $result = Invoke-Nexus -UriSlug $urislug -Method 'GET' - - $result | Foreach-Object { - [pscustomobject]@{ - Id = $_.id - Name = $_.name - } - } - } - } -} - -function Enable-NexusRealm { - <# - .SYNOPSIS - Enable realms in Nexus - - .DESCRIPTION - Enable realms in Nexus - - .PARAMETER Realm - The realms you wish to activate - - .EXAMPLE - Enable-NexusRealm -Realm 'NuGet Api-Key Realm', 'Rut Auth Realm' - .EXAMPLE - Enable-NexusRealm -Realm 'LDAP Realm' - - .NOTES - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/Enable-NexusRealm/')] - Param( - [Parameter(Mandatory)] - [ArgumentCompleter( { - param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) - - $r = (Get-NexusRealm).name - - if ($WordToComplete) { - $r.Where($_ -match "^$WordToComplete") - } - else { - $r - } - } - )] - [String[]] - $Realm - ) - - begin { - - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - $urislug = "/service/rest/v1/security/realms/active" - - } - - process { - - $collection = @() - - Get-NexusRealm -Active | ForEach-Object { $collection += $_.id } - - $Realm | Foreach-Object { - - switch ($_) { - 'Conan Bearer Token Realm' { $id = 'org.sonatype.repository.conan.internal.security.token.ConanTokenRealm' } - 'Default Role Realm' { $id = 'DefaultRole' } - 'Docker Bearer Token Realm' { $id = 'DockerToken' } - 'LDAP Realm' { $id = 'LdapRealm' } - 'Local Authentication Realm' { $id = 'NexusAuthenticatingRealm' } - 'Local Authorizing Realm' { $id = 'NexusAuthorizingRealm' } - 'npm Bearer Token Realm' { $id = 'NpmToken' } - 'NuGet API-Key Realm' { $id = 'NuGetApiKey' } - 'Rut Auth Realm' { $id = 'rutauth-realm' } - } - - $collection += $id - - } - - $body = $collection - - Write-Verbose $($Body | ConvertTo-Json) - $null = Invoke-Nexus -UriSlug $urislug -BodyAsArray $Body -Method PUT - - } -} - -function Get-NexusNuGetApiKey { - <# - .SYNOPSIS - Retrieves the NuGet API key of the given user credential - - .DESCRIPTION - Retrieves the NuGet API key of the given user credential - - .PARAMETER Credential - The Nexus User whose API key you wish to retrieve - - .EXAMPLE - Get-NexusNugetApiKey -Credential (Get-Credential) - - .NOTES - - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/Security/API%20Key/Get-NexusNuGetApiKey/')] - Param( - [Parameter(Mandatory)] - [PSCredential] - $Credential - ) - - process { - $token = Get-NexusUserToken -Credential $Credential - $base64Token = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($token)) - $UriBase = "$($protocol)://$($Hostname):$($port)" - - $slug = "/service/rest/internal/nuget-api-key?authToken=$base64Token&_dc=$(([DateTime]::ParseExact("01/02/0001 21:08:29", "MM/dd/yyyy HH:mm:ss",$null)).Ticks)" - - $uri = $UriBase + $slug - - Invoke-RestMethod -Uri $uri -Method GET -ContentType 'application/json' -Headers $header - - } -} - -function New-NexusRawComponent { - <# - .SYNOPSIS - Uploads a file to a Raw repository - - .DESCRIPTION - Uploads a file to a Raw repository - - .PARAMETER RepositoryName - The Raw repository to upload too - - .PARAMETER File - The file to upload - - .PARAMETER Directory - The directory to store the file on the repo - - .PARAMETER Name - The name of the file stored into the repo. Can be different than the file name being uploaded. - - .EXAMPLE - New-NexusRawComponent -RepositoryName GeneralFiles -File C:\temp\service.1234.log - .EXAMPLE - New-NexusRawComponent -RepositoryName GeneralFiles -File C:\temp\service.log -Directory logs - .EXAMPLE - New-NexusRawComponent -RepositoryName GeneralFile -File C:\temp\service.log -Directory logs -Name service.99999.log - - .NOTES - #> - [CmdletBinding()] - Param( - [Parameter(Mandatory)] - [String] - $RepositoryName, - - [Parameter(Mandatory)] - [String] - $File, - - [Parameter()] - [String] - $Directory, - - [Parameter()] - [String] - $Name = (Split-Path -Leaf $File) - ) - - process { - - if (-not $Directory) { - $urislug = "/repository/$($RepositoryName)/$($Name)" - } - else { - $urislug = "/repository/$($RepositoryName)/$($Directory)/$($Name)" - - } - $UriBase = "$($protocol)://$($Hostname):$($port)" - $Uri = $UriBase + $UriSlug - - - $params = @{ - Uri = $Uri - Method = 'PUT' - ContentType = 'text/plain' - InFile = $File - Headers = $header - UseBasicParsing = $true - } - - $null = Invoke-WebRequest @params - } -} - -function Get-NexusUser { - <# - .SYNOPSIS - Retrieve a list of users. Note if the source is not 'default' the response is limited to 100 users. - - .DESCRIPTION - Retrieve a list of users. Note if the source is not 'default' the response is limited to 100 users. - - .PARAMETER User - The username to fetch - - .PARAMETER Source - The source to fetch from - - .EXAMPLE - Get-NexusUser - - .EXAMPLE - Get-NexusUser -User bob - - .EXAMPLE - Get-NexusUser -Source default - - .NOTES - - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/NexuShell/Security/User/Get-NexusUser/')] - Param( - [Parameter()] - [String] - $User, - - [Parameter()] - [String] - $Source - ) - - begin { - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - } - - process { - $urislug = '/service/rest/v1/security/users' - - if ($User) { - $urislug = "/service/rest/v1/security/users?userId=$User" - } - - if ($Source) { - $urislug = "/service/rest/v1/security/users?source=$Source" - } - - if ($User -and $Source) { - $urislug = "/service/rest/v1/security/users?userId=$User&source=$Source" - } - - $result = Invoke-Nexus -Urislug $urislug -Method GET - - $result | Foreach-Object { - [pscustomobject]@{ - Username = $_.userId - FirstName = $_.firstName - LastName = $_.lastName - EmailAddress = $_.emailAddress - Source = $_.source - Status = $_.status - ReadOnly = $_.readOnly - Roles = $_.roles - ExternalRoles = $_.externalRoles - } - } - } -} - -function Get-NexusRole { - <# - .SYNOPSIS - Retrieve Nexus Role information - - .DESCRIPTION - Retrieve Nexus Role information - - .PARAMETER Role - The role to retrieve - - .PARAMETER Source - The source to retrieve from - - .EXAMPLE - Get-NexusRole - - .EXAMPLE - Get-NexusRole -Role ExampleRole - - .NOTES - - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/NexuShell/Security/Roles/Get-NexusRole/')] - Param( - [Parameter()] - [Alias('id')] - [String] - $Role, - - [Parameter()] - [String] - $Source - ) - begin { if (-not $header) { throw 'Not connected to Nexus server! Run Connect-NexusServer first.' } } - process { - - $urislug = '/service/rest/v1/security/roles' - - if ($Role) { - $urislug = "/service/rest/v1/security/roles/$Role" - } - - if ($Source) { - $urislug = "/service/rest/v1/security/roles?source=$Source" - } - - if ($Role -and $Source) { - $urislug = "/service/rest/v1/security/roles/$($Role)?source=$Source" - } - - Write-verbose $urislug - $result = Invoke-Nexus -Urislug $urislug -Method GET - - $result | ForEach-Object { - [PSCustomObject]@{ - Id = $_.id - Source = $_.source - Name = $_.name - Description = $_.description - Privileges = $_.privileges - Roles = $_.roles - } - } - } -} - -function New-NexusUser { - <# - .SYNOPSIS - Create a new user in the default source. - - .DESCRIPTION - Create a new user in the default source. - - .PARAMETER Username - The userid which is required for login. This value cannot be changed. - - .PARAMETER Password - The password for the new user. - - .PARAMETER FirstName - The first name of the user. - - .PARAMETER LastName - The last name of the user. - - .PARAMETER EmailAddress - The email address associated with the user. - - .PARAMETER Status - The user's status, e.g. active or disabled. - - .PARAMETER Roles - The roles which the user has been assigned within Nexus. - - .EXAMPLE - $params = @{ - Username = 'jimmy' - Password = ("sausage" | ConvertTo-SecureString -AsPlainText -Force) - FirstName = 'Jimmy' - LastName = 'Dean' - EmailAddress = 'sausageking@jimmydean.com' - Status = Active - Roles = 'nx-admin' - } - - New-NexusUser @params - - .NOTES - - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/NexuShell/Security/User/New-NexusUser/')] - Param( - [Parameter(Mandatory)] - [String] - $Username, - - [Parameter(Mandatory)] - [SecureString] - $Password, - - [Parameter(Mandatory)] - [String] - $FirstName, - - [Parameter(Mandatory)] - [String] - $LastName, - - [Parameter(Mandatory)] - [String] - $EmailAddress, - - [Parameter(Mandatory)] - [ValidateSet('Active', 'Locked', 'Disabled', 'ChangePassword')] - [String] - $Status, - - [Parameter(Mandatory)] - [ArgumentCompleter({ - param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) - (Get-NexusRole).Id.Where{ $_ -like "*$WordToComplete*" } - })] - [String[]] - $Roles - ) - - process { - $urislug = '/service/rest/v1/security/users' - - $Body = @{ - userId = $Username - firstName = $FirstName - lastName = $LastName - emailAddress = $EmailAddress - password = [System.Net.NetworkCredential]::new($Username, $Password).Password - status = $Status - roles = $Roles - } - - Write-Verbose ($Body | ConvertTo-Json) - $result = Invoke-Nexus -Urislug $urislug -Body $Body -Method POST - - [pscustomObject]@{ - Username = $result.userId - FirstName = $result.firstName - LastName = $result.lastName - EmailAddress = $result.emailAddress - Source = $result.source - Status = $result.status - Roles = $result.roles - ExternalRoles = $result.externalRoles - } - } -} - -function New-NexusRole { - <# - .SYNOPSIS - Creates a new Nexus Role - - .DESCRIPTION - Creates a new Nexus Role - - .PARAMETER Id - The ID of the role - - .PARAMETER Name - The friendly name of the role - - .PARAMETER Description - A description of the role - - .PARAMETER Privileges - Included privileges for the role - - .PARAMETER Roles - Included nested roles - - .EXAMPLE - New-NexusRole -Id SamepleRole - - .EXAMPLE - New-NexusRole -Id SampleRole -Description "A sample role" -Privileges nx-all - - .NOTES - - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/NexuShell/Security/Roles/New-NexusRole/')] - Param( - [Parameter(Mandatory)] - [String] - $Id, - - [Parameter(Mandatory)] - [String] - $Name, - - [Parameter()] - [String] - $Description, - - [Parameter(Mandatory)] - [String[]] - $Privileges, - - [Parameter()] - [String[]] - $Roles - ) - - begin { - if (-not $header) { - throw 'Not connected to Nexus server! Run Connect-NexusServer first.' - } - } - - process { - - $urislug = '/service/rest/v1/security/roles' - $Body = @{ - - id = $Id - name = $Name - description = $Description - privileges = @($Privileges) - roles = $Roles - - } - - Invoke-Nexus -Urislug $urislug -Body $Body -Method POST | Foreach-Object { - [PSCustomobject]@{ - Id = $_.id - Name = $_.name - Description = $_.description - Privileges = $_.privileges - Roles = $_.roles - } - } - - } -} - -function Set-NexusAnonymousAuth { - <# - .SYNOPSIS - Turns Anonymous Authentication on or off in Nexus - - .DESCRIPTION - Turns Anonymous Authentication on or off in Nexus - - .PARAMETER Enabled - Turns on Anonymous Auth - - .PARAMETER Disabled - Turns off Anonymous Auth - - .EXAMPLE - Set-NexusAnonymousAuth -Enabled - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/NexuShell/Set-NexusAnonymousAuth/')] - Param( - [Parameter()] - [Switch] - $Enabled, - - [Parameter()] - [Switch] - $Disabled - ) - - begin { - - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - $urislug = "/service/rest/v1/security/anonymous" - } - - process { - - Switch ($true) { - - $Enabled { - $Body = @{ - enabled = $true - userId = 'anonymous' - realmName = 'NexusAuthorizingRealm' - } - - Invoke-Nexus -UriSlug $urislug -Body $Body -Method 'PUT' - } - - $Disabled { - $Body = @{ - enabled = $false - userId = 'anonymous' - realmName = 'NexusAuthorizingRealm' - } - - Invoke-Nexus -UriSlug $urislug -Body $Body -Method 'PUT' - - } - } + try { + $scriptName = [GUID]::NewGuid().ToString() + New-NexusScript -Name $scriptName -Content $Script -Type "groovy" + Start-NexusScript -Name $scriptName + } finally { + Remove-NexusScript -Name $scriptName } } @@ -1827,7 +488,7 @@ function Remove-CcmBinding { process { Write-Verbose "Removing existing bindings" - netsh http delete sslcert ipport=0.0.0.0:443 + netsh http delete sslcert ipport=0.0.0.0:443 | Write-Verbose } } @@ -1839,7 +500,7 @@ function New-CcmBinding { Write-Verbose "Adding new binding https://${SubjectWithoutCn} to Chocolatey Central Management" $guid = [Guid]::NewGuid().ToString("B") - netsh http add sslcert ipport=0.0.0.0:443 certhash=$Thumbprint certstorename=TrustedPeople appid="$guid" + netsh http add sslcert ipport=0.0.0.0:443 certhash=$Thumbprint certstorename=TrustedPeople appid="$guid" | Write-Verbose Get-WebBinding -Name ChocolateyCentralManagement | Remove-WebBinding New-WebBinding -Name ChocolateyCentralManagement -Protocol https -Port 443 -SslFlags 0 -IpAddress '*' } diff --git a/tests/server.tests.ps1 b/tests/server.tests.ps1 index e05e667..fbd2940 100644 --- a/tests/server.tests.ps1 +++ b/tests/server.tests.ps1 @@ -45,8 +45,8 @@ Describe "Server Integrity" { $Logs = Get-ChildItem C:\choco-setup\logs -Recurse -Filter *.txt } - It " log file was created during installation" -Foreach @( - @{File = 'Set-SslCertificate'} + It " log file was created during installation" -ForEach @( + @{File = 'Set-SslSecurity'} @{File = 'Start-C4bCcmSetup'} @{File = 'Start-C4bJenkinsSetup'} @{File = 'Start-C4bNexusSetup'} From 8180a3ddc96b7ee87ecb262f916ef2743bfd53a5 Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Thu, 20 Feb 2025 16:36:55 +0000 Subject: [PATCH 03/17] (#291) Adds Nexus Configuration to Start-C4BNexus Configuring the Nexus users and (optionally) SSL earlier in the Nexus script allows us to use the correct users and FQDNs immediately, and result in a more immediately available environment. Now that we are always doing these actions (e.g. securing the repositories), this should also reduce a lot of confusion with what the SSL script does. --- Set-SslSecurity.ps1 | 53 +---- Start-C4bNexusSetup.ps1 | 211 +++++++++++++++--- Start-C4bSetup.ps1 | 18 +- modules/C4B-Environment/C4B-Environment.psm1 | 12 +- .../C4B-Environment/ReadmeTemplate.html.j2 | 22 +- tests/nexus.tests.ps1 | 3 +- 6 files changed, 213 insertions(+), 106 deletions(-) diff --git a/Set-SslSecurity.ps1 b/Set-SslSecurity.ps1 index bc3237f..6e205ef 100644 --- a/Set-SslSecurity.ps1 +++ b/Set-SslSecurity.ps1 @@ -107,16 +107,7 @@ process { Start-Service nexus Write-Warning "Waiting to give Nexus time to start up on 'https://${SubjectWithoutCn}:8443'" - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::tls12 - do { - $response = try { - Invoke-WebRequest "https://${SubjectWithoutCn}:8443" -UseBasicParsing -ErrorAction Stop - Start-Sleep -Seconds 3 - } catch {} - } until ($response.StatusCode -eq '200') - Write-Host "Nexus is ready!" - - Invoke-Choco source remove --name="'ChocolateyInternal'" + Wait-Nexus # Build Credential Object, Connect to Nexus if ($Credential = Get-ChocoEnvironmentProperty NexusCredential) { @@ -134,46 +125,7 @@ process { (Get-Content -Path $ClientScript) -replace "{{hostname}}", $SubjectWithoutCn | Set-Content -Path $ClientScript $null = New-NexusRawComponent -RepositoryName 'choco-install' -File $ClientScript - # Disable anonymous authentication - $null = Set-NexusAnonymousAuth -Disabled - - if (-not (Get-NexusRole -Role 'chocorole' -ErrorAction SilentlyContinue)) { - # Create Nexus role - $RoleParams = @{ - Id = "chocorole" - Name = "chocorole" - Description = "Role for web enabled choco clients" - Privileges = @('nx-repository-view-nuget-*-browse', 'nx-repository-view-nuget-*-read', 'nx-repository-view-raw-*-read', 'nx-repository-view-raw-*-browse') - } - $null = New-NexusRole @RoleParams - - $Timeout = [System.Diagnostics.Stopwatch]::StartNew() - while ($Timeout.Elapsed.TotalSeconds -lt 30 -and -not (Get-NexusRole -Role $RoleParams.Id -ErrorAction SilentlyContinue)) { - Start-Sleep -Seconds 3 - } - } - - if (-not (Get-NexusUser -User 'chocouser' -ErrorAction SilentlyContinue)) { - $NexusPw = [System.Web.Security.Membership]::GeneratePassword(32, 12) - # Create Nexus user - $UserParams = @{ - Username = 'chocouser' - Password = ($NexusPw | ConvertTo-SecureString -AsPlainText -Force) - FirstName = 'Choco' - LastName = 'User' - EmailAddress = 'chocouser@example.com' - Status = 'Active' - Roles = 'chocorole' - } - $null = New-NexusUser @UserParams - - $Timeout = [System.Diagnostics.Stopwatch]::StartNew() - while ($Timeout.Elapsed.TotalSeconds -lt 30 -and -not (Get-NexusUser -User $UserParams.Username -ErrorAction SilentlyContinue)) { - Start-Sleep -Seconds 3 - } - } else { - $NexusPw = Get-ChocoEnvironmentProperty ChocoUserPassword - } + $NexusPw = Get-ChocoEnvironmentProperty ChocoUserPassword -AsPlainText # Update all sources with credentials and the new path foreach ($Repository in Get-NexusRepository -Format nuget | Where-Object Type -eq 'hosted') { @@ -201,7 +153,6 @@ process { Update-Clixml -Properties @{ NexusUri = "https://$($SubjectWithoutCn):8443" NexusRepo = "https://${SubjectWithoutCn}:8443/repository/ChocolateyInternal/index.json" - ChocoUserPassword = $NexusPw } <# Jenkins #> diff --git a/Start-C4bNexusSetup.ps1 b/Start-C4bNexusSetup.ps1 index bba8ead..55b3796 100644 --- a/Start-C4bNexusSetup.ps1 +++ b/Start-C4bNexusSetup.ps1 @@ -15,16 +15,28 @@ C4B Quick-Start Guide Nexus setup script - Setup of firewall rule for repository access #> [CmdletBinding()] -param( - # Choice of non-IE broswer for Nexus +param( + # The certificate thumbprint that identifies the target SSL certificate in + # the local machine certificate stores. [Parameter()] + [ArgumentCompleter({ + Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { + [System.Management.Automation.CompletionResult]::new( + $_.Thumbprint, + $_.Thumbprint, + "ParameterValue", + ($_.Subject -replace "^CN=(?.+),?.*$",'${FQDN}') + ) + } + })] [string] - $Browser = 'Edge' + $Thumbprint ) process { $DefaultEap = $ErrorActionPreference $ErrorActionPreference = 'Stop' Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Start-C4bNexusSetup-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" + $NexusPort = 8081 $Packages = (Get-Content $PSScriptRoot\files\chocolatey.json | ConvertFrom-Json).packages @@ -36,34 +48,168 @@ process { $chocoArgs = @('install', 'nexushell', '-y' ,'--no-progress') & Invoke-Choco @chocoArgs - #Build Credential Object, Connect to Nexus + if ($Thumbprint) { + $NexusPort = 8443 + + $null = Set-NexusCert -Thumbprint $Thumbprint -Port $NexusPort + + Import-Module NexuShell + + if ($CertificateDnsName = Get-ChocoEnvironmentProperty CertSubject) { + & (Get-Module NexuShell) {Get-NexusUri -HostnameOverride $CertificateDnsName} | Write-Verbose + } + } + + # Add Nexus port access via firewall + $FwRuleParams = @{ + DisplayName = "Nexus Repository access on TCP $NexusPort" + Direction = 'Inbound' + LocalPort = $NexusPort + Protocol = 'TCP' + Action = 'Allow' + } + $null = New-NetFirewallRule @FwRuleParams + + Wait-Nexus + Write-Host "Configuring Sonatype Nexus Repository" - $securePw = (Get-Content 'C:\programdata\sonatype-work\nexus3\admin.password') | ConvertTo-SecureString -AsPlainText -Force - $Credential = [System.Management.Automation.PSCredential]::new('admin',$securePw) - Connect-NexusServer -Hostname localhost -Credential $Credential + # Build Credential Object, Connect to Nexus + if (-not ($Credential = Get-ChocoEnvironmentProperty NexusCredential)) { + Write-Host "Setting up admin account." + $NexusDefaultPasswordPath = 'C:\programdata\sonatype-work\nexus3\admin.password' + + $Timeout = [System.Diagnostics.Stopwatch]::StartNew() + while (-not (Test-Path $NexusDefaultPasswordPath) -and $Timeout.Elapsed.TotalMinutes -lt 3) { + Start-Sleep -Seconds 5 + } + + $DefaultNexusCredential = [System.Management.Automation.PSCredential]::new( + 'admin', + (Get-Content $NexusDefaultPasswordPath | ConvertTo-SecureString -AsPlainText -Force) + ) - #Drain default repositories + try { + Connect-NexusServer -LocalService -Credential $DefaultNexusCredential -ErrorAction Stop + + $Credential = [PSCredential]::new( + "admin", + (New-ServicePassword) + ) + + Set-NexusUserPassword -Username admin -NewPassword $Credential.Password -ErrorAction Stop + Set-ChocoEnvironmentProperty -Name NexusCredential -Value $Credential + } finally {} + + if (Test-Path $NexusDefaultPasswordPath) { + Remove-Item -Path $NexusDefaultPasswordPath + } + } + Connect-NexusServer -LocalService -Credential $Credential + + # Disable anonymous authentication + $null = Set-NexusAnonymousAuth -Disabled + + # Drain default repositories $null = Get-NexusRepository | Where-Object Name -NotLike "choco*" | Remove-NexusRepository -Force - #Enable NuGet Auth Realm + # Enable NuGet Auth Realm Enable-NexusRealm -Realm 'NuGet API-Key Realm' - #Create Chocolatey repositories - New-NexusNugetHostedRepository -Name ChocolateyInternal -DeploymentPolicy Allow - New-NexusNugetHostedRepository -Name ChocolateyTest -DeploymentPolicy Allow - New-NexusRawHostedRepository -Name choco-install -DeploymentPolicy Allow -ContentDisposition Attachment + # Create Chocolatey repositories + if (-not (Get-NexusRepository -Name ChocolateyInternal)) { + New-NexusNugetHostedRepository -Name ChocolateyInternal -DeploymentPolicy Allow + } - #Surface API Key - $NuGetApiKey = (Get-NexusNuGetApiKey -Credential $Credential).apikey + if (-not (Get-NexusRepository -Name ChocolateyTest)) { + New-NexusNugetHostedRepository -Name ChocolateyTest -DeploymentPolicy Allow + } - # Push all packages from previous steps to NuGet repo - Get-ChildItem -Path "$env:SystemDrive\choco-setup\files\files" -Filter *.nupkg | ForEach-Object { - Invoke-Choco push $_.FullName --source "$((Get-NexusRepository -Name 'ChocolateyInternal').url)/index.json" --apikey $NugetApiKey --force + if (-not (Get-NexusRepository -Name choco-install)) { + New-NexusRawHostedRepository -Name choco-install -DeploymentPolicy Allow -ContentDisposition Attachment } - # Temporary workaround to reset the NuGet v3 cache, such that it doesn't capture localhost as the FQDN - Remove-NexusRepositoryFolder -RepositoryName ChocolateyInternal -Name v3 + # Create role for end user to pull from Nexus + if (-not ($NexusRole = Get-NexusRole -Role 'chocorole' -ErrorAction SilentlyContinue)) { + # Create Nexus role + $RoleParams = @{ + Id = "chocorole" + Name = "chocorole" + Description = "Role for web enabled choco clients" + Privileges = @('nx-repository-view-nuget-*-browse', 'nx-repository-view-nuget-*-read', 'nx-repository-view-raw-*-read', 'nx-repository-view-raw-*-browse') + } + $NexusRole = New-NexusRole @RoleParams + + $Timeout = [System.Diagnostics.Stopwatch]::StartNew() + while ($Timeout.Elapsed.TotalSeconds -lt 30 -and -not (Get-NexusRole -Role $RoleParams.Id -ErrorAction SilentlyContinue)) { + Start-Sleep -Seconds 3 + } + } + + # Create new user for endpoints + if (-not (Get-NexusUser -User 'chocouser' -ErrorAction SilentlyContinue)) { + $NexusPw = [System.Web.Security.Membership]::GeneratePassword(32, 12) + # Create Nexus user + $UserParams = @{ + Username = 'chocouser' + Password = ($NexusPw | ConvertTo-SecureString -AsPlainText -Force) + FirstName = 'Choco' + LastName = 'User' + EmailAddress = 'chocouser@example.com' + Status = 'Active' + Roles = $NexusRole.Id + } + $null = New-NexusUser @UserParams + + $Timeout = [System.Diagnostics.Stopwatch]::StartNew() + while ($Timeout.Elapsed.TotalSeconds -lt 30 -and -not (Get-NexusUser -User $UserParams.Username -ErrorAction SilentlyContinue)) { + Start-Sleep -Seconds 3 + } + + Set-ChocoEnvironmentProperty ChocoUserPassword $UserParams.Password + } + + # Create role for task runner to push to Nexus + if (-not ($PackageUploadRole = Get-NexusRole -Role "package-uploader" -ErrorAction SilentlyContinue)) { + $PackageUploadRole = New-NexusRole -Name "package-uploader" -Id "package-uploader" -Description "Role allowed to push and list packages" -Privileges @( + "nx-repository-view-nuget-*-edit" + "nx-repository-view-nuget-*-read" + "nx-apikey-all" + ) + } + + # Create new user for package-upload - as this changes the usercontext, ensure this is the last thing in the script, or it's in a job + if ($UploadUser = Get-ChocoEnvironmentProperty PackageUploadCredential) { + Write-Verbose "Using existing PackageUpload credential '$($UploadUser.UserName)'" + } else { + $UploadUser = [PSCredential]::new( + 'chocoPackager', + (New-ServicePassword -Length 64) + ) + } + + if (-not (Get-NexusUser -User $UploadUser.UserName)) { + $NewUser = @{ + Username = $UploadUser.UserName + Password = $UploadUser.Password + FirstName = "Chocolatey" + LastName = "Packager" + EmailAddress = "packager@$env:ComputerName.local" + Status = "Active" + Roles = $PackageUploadRole.Id + } + $null = New-NexusUser @NewUser + + Set-ChocoEnvironmentProperty -Name PackageUploadCredential -Value $UploadUser + } + + # Retrieve the API Key to use in Jenkins et al + if ($NuGetApiKey = Get-ChocoEnvironmentProperty PackageApiKey) { + Write-Verbose "Using existing Nexus Api Key for '$($UploadUser.UserName)'" + } else { + $NuGetApiKey = (Get-NexusNuGetApiKey -Credential $UploadUser).apiKey + Set-ChocoEnvironmentProperty -Name PackageApiKey -Value $NuGetApiKey + } # Push latest ChocolateyInstall.ps1 to raw repo $ScriptDir = "$env:SystemDrive\choco-setup\files\scripts" @@ -85,12 +231,21 @@ process { Invoke-Choco feature disable --name="'usePackageRepositoryOptimizations'" # Add ChocolateyInternal as a source repository - Invoke-Choco source add -n 'ChocolateyInternal' -s "$((Get-NexusRepository -Name 'ChocolateyInternal').url)/index.json" --priority 1 + $LocalSource = "$((Get-NexusRepository -Name 'ChocolateyInternal').url)/index.json" + Invoke-Choco source add -n 'ChocolateyInternal' -s $LocalSource -u="$($UploadUser.UserName)" -p="$($UploadUser.GetNetworkCredential().Password)" --priority 1 # Add ChocolateyTest as a source repository, to enable authenticated pushing - Invoke-Choco source add -n 'ChocolateyTest' -s "$((Get-NexusRepository -Name 'ChocolateyTest').url)/index.json" + Invoke-Choco source add -n 'ChocolateyTest' -s "$((Get-NexusRepository -Name 'ChocolateyTest').url)/index.json" -u="$($UploadUser.UserName)" -p="$($UploadUser.GetNetworkCredential().Password)" Invoke-Choco source disable -n 'ChocolateyTest' + # Push all packages from previous steps to NuGet repo + Get-ChildItem -Path "$env:SystemDrive\choco-setup\files\files" -Filter *.nupkg | ForEach-Object { + Invoke-Choco push $_.FullName --source $LocalSource --apikey $NugetApiKey --force + } + + # Temporary workaround to reset the NuGet v3 cache, such that it doesn't capture localhost as the FQDN + Remove-NexusRepositoryFolder -RepositoryName ChocolateyInternal -Name v3 + # Remove Local Chocolatey Setup Source $chocoArgs = @('source', 'remove', '--name="LocalChocolateySetup"') & Invoke-Choco @chocoArgs @@ -113,19 +268,9 @@ process { } } - # Add Nexus port 8081 access via firewall - $FwRuleParams = @{ - DisplayName = 'Nexus Repository access on TCP 8081' - Direction = 'Inbound' - LocalPort = 8081 - Protocol = 'TCP' - Action = 'Allow' - } - $null = New-NetFirewallRule @FwRuleParams - # Save useful params Update-Clixml -Properties @{ - NexusUri = "http://localhost:8081" + NexusUri = & (Get-Module NexuShell) {Get-NexusUri} NexusCredential = $Credential NexusRepo = "$((Get-NexusRepository -Name 'ChocolateyInternal').url)/index.json" NuGetApiKey = $NugetApiKey | ConvertTo-SecureString -AsPlainText -Force diff --git a/Start-C4bSetup.ps1 b/Start-C4bSetup.ps1 index 9205514..e9d86a2 100644 --- a/Start-C4bSetup.ps1 +++ b/Start-C4bSetup.ps1 @@ -52,7 +52,9 @@ param( [Parameter(ParameterSetName='Unattended')] [System.Management.Automation.PSCredential] $DatabaseCredential = $( - if ($PSCmdlet.ParameterSetName -eq 'Unattended') { + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseCredential) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseCredential + } elseif ($PSCmdlet.ParameterSetName -eq 'Unattended') { $Wshell = New-Object -ComObject Wscript.Shell $null = $Wshell.Popup('You will now create a credential for the ChocolateyManagement DB user, to be used by CCM (document this somewhere).') Get-Credential -UserName ChocoUser -Message 'Create a credential for the ChocolateyManagement DB user' @@ -138,6 +140,18 @@ try { } Import-Module C4B-Environment -Verbose:$false + Update-Clixml -Properties @{ + InitialDeployment = Get-Date + } + + if ($Thumbprint) { + Set-ChocoEnvironmentProperty Thumbprint $Thumbprint + } + + if ($DatabaseCredential) { + Set-ChocoEnvironmentProperty DatabaseCredential $DatabaseCredential + } + # Downloading all CCM setup packages below Write-Host "Downloading missing nupkg files to $($PkgsDir)." -ForegroundColor Green Write-Host "This will take some time. Feel free to get a tea or coffee." -ForegroundColor Green @@ -164,7 +178,7 @@ try { if ($Thumbprint) {$Certificate.Thumbprint = $Thumbprint} Set-Location "$env:SystemDrive\choco-setup\files" - .\Start-C4BNexusSetup.ps1 + .\Start-C4BNexusSetup.ps1 @Certificate .\Start-C4bCcmSetup.ps1 @Certificate -DatabaseCredential $DatabaseCredential .\Start-C4bJenkinsSetup.ps1 .\Set-SslSecurity.ps1 @Certificate diff --git a/modules/C4B-Environment/C4B-Environment.psm1 b/modules/C4B-Environment/C4B-Environment.psm1 index 72e0654..049a3ac 100644 --- a/modules/C4B-Environment/C4B-Environment.psm1 +++ b/modules/C4B-Environment/C4B-Environment.psm1 @@ -919,16 +919,18 @@ The host name of the C4B instance. "{{ nexus_fqdn .*?}}" = ([uri]$Data.NexusUri).DnsSafeHost "{{ nexus_port .*?}}" = ([uri]$Data.NexusUri).Port "{{ nexus_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.NexusCredential.Password.ToPlainText()) - "{{ lookup\('file', 'credentials\/nexus_apikey'\) .*?}}" = Get-ChocoEnvironmentProperty NugetApiKey -AsPlainText + "{{ lookup\('file', 'credentials\/nexus_apikey'\) .*?}}" = $Data.NugetApiKey.ToPlainText() + + "{{ nexus_client_username .*?}}" = 'chocouser' + "{{ nexus_client_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.ChocoUserPassword.ToPlainText()) + + "{{ nexus_packager_username .*?}}" = $Data.PackageUploadCredential.Username + "{{ nexus_packager_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.PackageUploadCredential.Password.ToPlainText()) # Jenkins Values "{{ jenkins_fqdn .*?}}" = ([uri]$Data.JenkinsUri).DnsSafeHost "{{ jenkins_port .*?}}" = ([uri]$Data.JenkinsUri).Port "{{ jenkins_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.JenkinsCredential.Password.ToPlainText()) - - # Nexus Chocolatey Source Credential values - "{{ nexus_client_username .*?}}" = 'chocouser' - "{{ nexus_client_password .*?}}" = $Data.ChocoUserPassword } } } diff --git a/modules/C4B-Environment/ReadmeTemplate.html.j2 b/modules/C4B-Environment/ReadmeTemplate.html.j2 index d43a8fa..a0f9a94 100644 --- a/modules/C4B-Environment/ReadmeTemplate.html.j2 +++ b/modules/C4B-Environment/ReadmeTemplate.html.j2 @@ -158,21 +158,23 @@ function CopyToClipboard(id)

Accounts and Secrets

- + - - - + + - + + + + @@ -180,7 +182,6 @@ function CopyToClipboard(id) -
NameUrlUsernamePasswordApiKey
NameUrlUsernamePassword
Chocolatey Central Management {{ ccm_fqdn }}:{{ ccm_port }} ccmadmin
{{ ccm_password | e }}
N/A
Nexus{{ nexus_fqdn }}:{{ nexus_port }}Nexus{{ nexus_fqdn }}:{{ nexus_port }} admin
{{ nexus_password | e }}
{{ lookup('file', 'credentials/nexus_apikey') | default('Unavailable') }}
{{ nexus_client_username }}
{{ nexus_client_password | e }}
{{ jenkins_fqdn }}:{{ jenkins_port }} admin
{{ jenkins_password | e }}
N/A


@@ -200,14 +201,9 @@ function CopyToClipboard(id)
{{ ccm_service_salt | e }}
- Nexus Repository Source Username -
{{ nexus_client_username | e }}
+ Chocolatey Package Uploader API Key +
{{ lookup('file', 'credentials/nexus_apikey') | default('Unavailable') }}
- - Nexus Repository Source Password -
{{ nexus_client_password | e }}
- -

📝 Note

diff --git a/tests/nexus.tests.ps1 b/tests/nexus.tests.ps1 index df30c15..aa9f991 100644 --- a/tests/nexus.tests.ps1 +++ b/tests/nexus.tests.ps1 @@ -57,8 +57,7 @@ Describe "Nexus Configuration" { Context "Repository Configuration" { BeforeAll { - $password = (Get-Content 'C:\ProgramData\sonatype-work\nexus3\admin.password') | ConvertTo-SecureString -AsPlainText -Force - $credential = [System.Management.Automation.PSCredential]::new('admin',$password) + $credential = Get-ChocoEnvironmentProperty NexusCredential . "C:\choco-setup\files\scripts\Get-Helpers.ps1" $null = Connect-NexusServer -Hostname $Fqdn -Credential $credential -UseSSL From 3ebe0598496067afe4291f4c24e4e7166b69595d Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Fri, 21 Feb 2025 12:10:13 +0000 Subject: [PATCH 04/17] (#291) Adds Jenkins HTTPS Configuration in Start-C4BJenkins Brings Jenkins configuration from the Set-SSL script to the Jenkins script, resulting in a correctly configured installation. --- Set-SslSecurity.ps1 | 18 ++++++-- Start-C4bJenkinsSetup.ps1 | 45 +++++++++++++++++--- Start-C4bSetup.ps1 | 2 +- modules/C4B-Environment/C4B-Environment.psm1 | 22 ++++++++++ 4 files changed, 75 insertions(+), 12 deletions(-) diff --git a/Set-SslSecurity.ps1 b/Set-SslSecurity.ps1 index 6e205ef..0680828 100644 --- a/Set-SslSecurity.ps1 +++ b/Set-SslSecurity.ps1 @@ -157,17 +157,27 @@ process { <# Jenkins #> $JenkinsHome = "C:\ProgramData\Jenkins\.jenkins" + $JenkinsPort = 7443 - Set-JenkinsLocationConfiguration -Url "https://$($SubjectWithoutCn):7443" -Path $JenkinsHome\jenkins.model.JenkinsLocationConfiguration.xml + Set-JenkinsLocationConfiguration -Url "https://$($SubjectWithoutCn):$($JenkinsPort)" -Path $JenkinsHome\jenkins.model.JenkinsLocationConfiguration.xml # Generate Jenkins keystore - Set-JenkinsCertificate -Thumbprint $Certificate.Thumbprint + Set-JenkinsCertificate -Thumbprint $Certificate.Thumbprint -Port $JenkinsPort # Add firewall rule for Jenkins - netsh advfirewall firewall add rule name="Jenkins-7443" dir=in action=allow protocol=tcp localport=7443 + netsh advfirewall firewall add rule name="Jenkins-$($JenkinsPort)" dir=in action=allow protocol=tcp localport=$JenkinsPort + + # Update job parameters in Jenkins + $NexusUri = Get-ChocoEnvironmentProperty NexusUri + Update-JenkinsJobParameters -Replacement @{ + "P_DST_URL" = "$NexusUri/repository/ChocolateyTest/index.json" + "P_LOCAL_REPO_URL" = "$NexusUri/repository/ChocolateyTest/index.json" + "P_TEST_REPO_URL" = "$NexusUri/repository/ChocolateyTest/index.json" + "P_PROD_REPO_URL" = "$NexusUri/repository/ChocolateyInternal/index.json" + } Update-Clixml -Properties @{ - JenkinsUri = "https://$($SubjectWithoutCn):7443" + JenkinsUri = "https://$($SubjectWithoutCn):$($JenkinsPort)" } <# CCM #> diff --git a/Start-C4bJenkinsSetup.ps1 b/Start-C4bJenkinsSetup.ps1 index 2806de2..b46448c 100644 --- a/Start-C4bJenkinsSetup.ps1 +++ b/Start-C4bJenkinsSetup.ps1 @@ -18,6 +18,26 @@ param( [string]$NuGetApiKey = $( if (-not (Get-Command Get-ChocoEnvironmentProperty -ErrorAction SilentlyContinue)) {. $PSScriptRoot\scripts\Get-Helpers.ps1} Get-ChocoEnvironmentProperty NuGetApiKey -AsPlainText + ), + + # The certificate thumbprint that identifies the target SSL certificate in + # the local machine certificate stores. + [Parameter(ValueFromPipeline, ParameterSetName='Thumbprint')] + [ArgumentCompleter({ + Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { + [System.Management.Automation.CompletionResult]::new( + $_.Thumbprint, + $_.Thumbprint, + "ParameterValue", + ($_.Subject -replace "^CN=(?.+),?.*$",'${FQDN}') + ) + } + })] + [string] + $Thumbprint = $( + Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { + $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed + } | Select-Object -ExpandProperty Thumbprint -First 1 ) ) process { @@ -25,6 +45,8 @@ process { $ErrorActionPreference = 'Stop' Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Start-C4bJenkinsSetup-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" + $JenkinsScheme, $JenkinsPort = "http", "8080" + # Install temurin21jre to meet JRE>11 dependency of Jenkins $chocoArgs = @('install', 'temurin21jre', "--source='ChocolateyInternal'", '-y', '--no-progress', "--params='/ADDLOCAL=FeatureJavaHome'") & Invoke-Choco @chocoArgs @@ -47,18 +69,27 @@ process { $JenkinsVersion | Out-File -FilePath $JenkinsHome\jenkins.install.UpgradeWizard.state -Encoding utf8 $JenkinsVersion | Out-File -FilePath $JenkinsHome\jenkins.install.InstallUtil.lastExecVersion -Encoding utf8 - # Set the hostname, such that it's ready for use. - Set-JenkinsLocationConfiguration -Url "http://$($HostName):8080" -Path $JenkinsHome\jenkins.model.JenkinsLocationConfiguration.xml - #region Set Jenkins Password $JenkinsCred = Set-JenkinsPassword -UserName 'admin' -NewPassword $(New-ServicePassword) -PassThru #endregion - # Set home directory of Jenkins install - $JenkinsHome = 'C:\ProgramData\Jenkins\.jenkins' - Stop-Service -Name Jenkins + if ($Thumbprint) { + $JenkinsScheme, $JenkinsPort = "https", 7443 + + if ($SubjectWithoutCn = Get-ChocoEnvironmentProperty CertSubject) { + $Hostname = $SubjectWithoutCn + } + + # Generate Jenkins keystore + Set-JenkinsCertificate -Thumbprint $Thumbprint -Port $JenkinsPort + + # Add firewall rule for Jenkins + netsh advfirewall firewall add rule name="Jenkins-$($JenkinsPort)" dir=in action=allow protocol=tcp localport=$JenkinsPort + } + Set-JenkinsLocationConfiguration -Url "$($JenkinsScheme)://$($SubjectWithoutCn):$($JenkinsPort)" -Path $JenkinsHome\jenkins.model.JenkinsLocationConfiguration.xml + #region Jenkins Plugin Install & Update $JenkinsPlugins = (Get-Content $PSScriptRoot\files\jenkins.json | ConvertFrom-Json).plugins @@ -104,7 +135,7 @@ process { # Save useful params Update-Clixml -Properties @{ - JenkinsUri = "http://$($HostName):8080" + JenkinsUri = "$($JenkinsScheme)://$($HostName):$($JenkinsPort)" JenkinsCredential = $JenkinsCred } diff --git a/Start-C4bSetup.ps1 b/Start-C4bSetup.ps1 index e9d86a2..a011961 100644 --- a/Start-C4bSetup.ps1 +++ b/Start-C4bSetup.ps1 @@ -180,7 +180,7 @@ try { Set-Location "$env:SystemDrive\choco-setup\files" .\Start-C4BNexusSetup.ps1 @Certificate .\Start-C4bCcmSetup.ps1 @Certificate -DatabaseCredential $DatabaseCredential - .\Start-C4bJenkinsSetup.ps1 + .\Start-C4bJenkinsSetup.ps1 @Certificate .\Set-SslSecurity.ps1 @Certificate } } finally { diff --git a/modules/C4B-Environment/C4B-Environment.psm1 b/modules/C4B-Environment/C4B-Environment.psm1 index 049a3ac..118155e 100644 --- a/modules/C4B-Environment/C4B-Environment.psm1 +++ b/modules/C4B-Environment/C4B-Environment.psm1 @@ -877,6 +877,28 @@ function Set-JenkinsCertificate { Restart-Service Jenkins } } + +function Update-JenkinsJobParameters { + param( + [string]$JobsPath = "C:\ProgramData\Jenkins\.jenkins\jobs", + + [hashtable]$Replacement = @{} + ) + process { + foreach ($Job in Get-ChildItem $JobsPath -Filter config.xml -Recurse) { + Write-Verbose "Updating parameters in '$($Job.DirectoryName)'" + [xml]$Config = (Get-Content $Job.FullName) -replace "^\<\?xml version=['""]1\.1['""]"," Date: Mon, 24 Feb 2025 13:01:40 +0000 Subject: [PATCH 05/17] (#291) Adds CCM HTTPS Configuration in Start-C4BCCM --- Set-SslSecurity.ps1 | 14 +- Start-C4bCcmSetup.ps1 | 60 +++++- Start-C4bSetup.ps1 | 25 ++- modules/C4B-Environment/C4B-Environment.psm1 | 192 ++++++++++++++++++- 4 files changed, 268 insertions(+), 23 deletions(-) diff --git a/Set-SslSecurity.ps1 b/Set-SslSecurity.ps1 index 0680828..fa8a667 100644 --- a/Set-SslSecurity.ps1 +++ b/Set-SslSecurity.ps1 @@ -199,18 +199,8 @@ process { # Generate Register-C4bEndpoint.ps1 $EndpointScript = "$PSScriptRoot\scripts\Register-C4bEndpoint.ps1" - - if ($ClientSaltValue = Get-ChocoEnvironmentProperty ClientSalt -AsPlainText) { - Write-Verbose "Using stored client salt value." - } else { - $ClientSaltValue = New-CCMSalt - } - - if ($ServiceSaltValue = Get-ChocoEnvironmentProperty ClientSalt -AsPlainText) { - Write-Verbose "Using stored service salt value." - } else { - $ServiceSaltValue = New-CCMSalt - } + $ClientSaltValue = Get-ChocoEnvironmentProperty ClientSalt -AsPlainText + $ServiceSaltValue = Get-ChocoEnvironmentProperty ServiceSalt -AsPlainText Invoke-TextReplacementInFile -Path $EndpointScript -Replacement @{ "{{ ClientSaltValue }}" = $ClientSaltValue diff --git a/Start-C4bCcmSetup.ps1 b/Start-C4bCcmSetup.ps1 index 0c1f0cf..bf1fcee 100644 --- a/Start-C4bCcmSetup.ps1 +++ b/Start-C4bCcmSetup.ps1 @@ -15,7 +15,13 @@ param( [Parameter()] [ValidateNotNull()] [System.Management.Automation.PSCredential] - $DatabaseCredential = (Get-Credential -Username ChocoUser -Message 'Create a credential for the ChocolateyManagement DB user (document this somewhere)'), + $DatabaseCredential = ( + if ($DatabaseCredential = Get-ChocoEnvironmentProperty DatabaseUser) { + $DatabaseCredential + } else { + Get-Credential -Username ChocoUser -Message 'Create a credential for the ChocolateyManagement DB user (document this somewhere)' + } + ), # Certificate to use for CCM service [Parameter()] @@ -130,6 +136,7 @@ process { if (-not $hostName.EndsWith($domainName)) { $hostName += "." + $domainName } + $CcmEndpoint = "http://$hostName" Write-Host "Installing Chocolatey Central Management Service" $chocoArgs = @('install', 'chocolatey-management-service', "--source='ChocolateyInternal'", '-y', "--package-parameters-sensitive=`"/ConnectionString:'Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;User ID=$DatabaseUser;Password=$DatabaseUserPw;'`"", '--no-progress') @@ -153,12 +160,57 @@ process { $chocoArgs = @('install', 'chocolatey-management-web', "--source='ChocolateyInternal'", '-y', "--package-parameters-sensitive=""'/ConnectionString:Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;User ID=$DatabaseUser;Password=$DatabaseUserPw;'""", '--no-progress') & Invoke-Choco @chocoArgs + # Setup Website SSL + if ($Thumbprint) { + Stop-CcmService + Remove-CcmBinding + New-CcmBinding -Thumbprint $Thumbprint + Start-CcmService + + $CcmEndpoint = "https://$(Get-ChocoEnvironmentProperty CertSubject)" + } + + # Create the site hosting the certificate import script on port 80 + if ($MyCertificate.NotAfter -gt (Get-Date).AddYears(5)) { + .\scripts\New-IISCertificateHost.ps1 + } + + # Run initial configuration for CCM Admin + if (-not ($CCMCredential = Get-ChocoEnvironmentProperty CCMCredential)) { + $CCMCredential = [PSCredential]::new( + "ccmadmin", + (New-ServicePassword) + ) + Set-CcmAccountPassword -CcmEndpoint $CcmEndpoint -ConnectionString "Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;User ID=$DatabaseUser;Password=$DatabaseUserPw;" -NewPassword $CCMCredential.Password + Set-ChocoEnvironmentProperty CCMCredential $CCMCredential + } + + if (-not ($CCMEncryptionPassword = Get-ChocoEnvironmentProperty CCMEncryptionPassword)) { + $CCMEncryptionPassword = New-ServicePassword + + Set-CcmEncryptionPassword -CcmEndpoint $CcmEndpoint -Credential $CCMCredential -NewPassword $CCMEncryptionPassword + Set-ChocoEnvironmentProperty CCMEncryptionPassword $CCMEncryptionPassword + } + + # Set Client and Service salts + if (-not (Get-ChocoEnvironmentProperty ClientSalt)) { + $ClientSaltValue = New-CCMSalt + Set-ChocoEnvironmentProperty ClientSalt (ConvertTo-SecureString $ClientSaltValue -AsPlainText -Force) + + Invoke-Choco config set centralManagementClientCommunicationSaltAdditivePassword $ClientSaltValue + } + + if (-not (Get-ChocoEnvironmentProperty ServiceSalt)) { + $ServiceSaltValue = New-CCMSalt + Set-ChocoEnvironmentProperty ServiceSalt (ConvertTo-SecureString $ServiceSaltValue -AsPlainText -Force) + + Invoke-Choco config set centralManagementServiceCommunicationSaltAdditivePassword $ServiceSaltValue + } + $CcmSvcUrl = Invoke-Choco config get centralManagementServiceUrl -r Update-Clixml -Properties @{ CCMServiceURL = $CcmSvcUrl - CCMWebPortal = "http://localhost/Account/Login" - DefaultUser = "ccmadmin" - DefaultPwToBeChanged = "123qwe" + CCMWebPortal = "$CcmEndpoint/Account/Login" CCMDBUser = $DatabaseUser CCMInstallUser = whoami } diff --git a/Start-C4bSetup.ps1 b/Start-C4bSetup.ps1 index a011961..013a342 100644 --- a/Start-C4bSetup.ps1 +++ b/Start-C4bSetup.ps1 @@ -41,7 +41,7 @@ param( } ), - # Unattended mode. Allows you to skip running the other scripts indiviually. + # Unattended mode. Allows you to skip running the other scripts individually. [Parameter(Mandatory, ParameterSetName='Unattended')] [switch] $Unattend, @@ -145,11 +145,30 @@ try { } if ($Thumbprint) { - Set-ChocoEnvironmentProperty Thumbprint $Thumbprint + Set-ChocoEnvironmentProperty CertThumbprint $Thumbprint + + # Collect current certificate configuration + $Certificate = Get-Certificate -Thumbprint $Thumbprint + Copy-CertToStore -Certificate $Certificate + + if (-not ($CertificateDnsName = Get-ChocoEnvironmentProperty CertSubject)) { + $matcher = 'CN\s?=\s?(?[^,\s]+)' + $null = $Certificate.Subject -match $matcher + $CertificateDnsName = if ($Matches.Subject.StartsWith('*')) { + # This is a wildcard cert, we need to prompt for the intended CertificateDnsName + while ($CertificateDnsName -notlike $Matches.Subject) { + $CertificateDnsName = Read-Host -Prompt "$(if ($CertificateDnsName) {"'$($CertificateDnsName)' is not a subdomain of '$($Matches.Subject)'. "})Please provide an FQDN to use with the certificate '$($Matches.Subject)'" + } + $CertificateDnsName + } else { + $Matches.Subject + } + Set-ChocoEnvironmentProperty CertSubject $CertificateDnsName + } } if ($DatabaseCredential) { - Set-ChocoEnvironmentProperty DatabaseCredential $DatabaseCredential + Set-ChocoEnvironmentProperty DatabaseUser $DatabaseCredential } # Downloading all CCM setup packages below diff --git a/modules/C4B-Environment/C4B-Environment.psm1 b/modules/C4B-Environment/C4B-Environment.psm1 index 118155e..31d03ae 100644 --- a/modules/C4B-Environment/C4B-Environment.psm1 +++ b/modules/C4B-Environment/C4B-Environment.psm1 @@ -538,6 +538,190 @@ function Set-CcmCertificate { } } +function Get-CcmAuthenticatedSession { + [CmdletBinding()] + [OutputType([Microsoft.PowerShell.Commands.WebRequestSession])] + param( + # The CCM server to operate against + [string]$CcmEndpoint = "http://localhost", + + # The current credential for the account to change + [System.Net.NetworkCredential]$Credential = @{ + userName = "ccmadmin" + password = "123qwe" + } + ) + end { + # Wait-CCM -Url $CcmEndpoint + + Write-Verbose "Authenticating to CCM Web at '$($CcmEndpoint)'" + $methodParams = @{ + Uri = "$CcmEndpoint/Account/Login" + Body = @{ + usernameOrEmailAddress = $Credential.Username + password = $Credential.Password + } + ContentType = 'application/x-www-form-urlencoded' + Method = "POST" + SessionVariable = "Session" + } + try { + $null = Invoke-WebRequest @methodParams -UseBasicParsing -ErrorAction Stop + } catch { + Write-Error "Failed to authenticate with '$($CcmEndpoint)': $($_)" + } + + $Session + } +} + +function Set-CcmAccountPassword { + <# + .Synopsis + Sets the password for a current CCM user + + .Notes + Relies on the account not being set to reset-password-on-next-login, and not locked out. + #> + [CmdletBinding()] + param( + # The CCM server to operate against + [string]$CcmEndpoint = "http://localhost", + + # The current credential for the account to change + [System.Net.NetworkCredential]$Credential = @{ + userName = "ccmadmin" + password = "123qwe" + }, + + # A Valid ConnectionString for the CCM Database + [string]$ConnectionString, + + # The new password to set + [Parameter(Mandatory)] + [SecureString]$NewPassword + ) + $NewCredential = [System.Net.NetworkCredential]::new($Credential.UserName, $NewPassword) + + if ($ConnectionString) { + try { + $Connection = [System.Data.SQLClient.SqlConnection]::new($ConnectionString) + $Connection.Open() + $Query = [System.Data.SQLClient.SqlCommand]::new( + "UPDATE [dbo].[AbpUsers] SET ShouldChangePasswordOnNextLogin = 0, IsLockoutEnabled = 0 WHERE Name = @UserName and TenantId = '1'", + $Connection + ) + $null = $Query.Parameters.Add( + [System.Data.SqlClient.SqlParameter]::new('UserName', $Credential.UserName) + ) + $QueryResult = $Query.BeginExecuteReader() + while (-not $QueryResult.isCompleted) { + Write-Verbose "Waiting for SQL Query to return" + Start-Sleep -Milliseconds 100 + } + if ($QueryResult.isCompleted -and -not $QueryResult.IsFaulted) { + Write-Verbose "Unset ShouldChangePasswordOnNextLogin for '$($Credential.Username)'" + } + } finally { + $Query.Dispose() + $Connection.Close() + $Connection.Dispose() + } + } + + $Session = Get-CcmAuthenticatedSession -CcmEndpoint $CcmEndpoint -Credential $Credential + + Write-Verbose "Changing password for account '$($Credential.UserName)'" + $resetParams = @{ + Uri = "$CcmEndpoint/api/services/app/Profile/ChangePassword" + Body = @{ + CurrentPassword = $Credential.Password + NewPassword = $NewCredential.Password + NewPasswordRepeat = $NewCredential.Password + } | ConvertTo-Json + ContentType = 'application/json' + Method = "POST" + WebSession = $Session + } + $Result = Invoke-RestMethod @resetParams -UseBasicParsing + + if ($Result.Success -eq 'true') { + Write-Verbose "Password for account '$($Credential.UserName)' was changed successfully." + } +} + +function Update-CcmSettings { + [CmdletBinding()] + param( + # The CCM server to operate against + [string]$CcmEndpoint = "http://localhost", + + # The current credential for the admin account + [System.Net.NetworkCredential]$Credential = @{ + userName = "ccmadmin" + password = "123qwe" + }, + + # A hashtable of settings to update. Only works two levels deep. + [hashtable]$Settings + ) + end { + $Session = Get-CcmAuthenticatedSession -CcmEndpoint $CcmEndpoint -Credential $Credential + + # Get Current Settings + $ServerSettings = (Invoke-RestMethod -Uri $CcmEndpoint/api/services/app/TenantSettings/GetAllSettings -WebSession $Session).result + + # Overwrite Settings via Hashtable + foreach ($Heading in $Settings.Keys) { + foreach ($Setting in $Settings[$Heading].Keys) { + $ServerSettings.$Heading.$Setting = $Settings.$Heading.$Setting + } + } + + # PUT new Settings to CCM + $SettingChange = @{ + Uri = "$CcmEndpoint/api/services/app/TenantSettings/UpdateAllSettings" + Method = "PUT" + ContentType = 'application/json; charset=utf-8' + Body = $ServerSettings | ConvertTo-Json + WebSession = $Session + } + $Result = Invoke-RestMethod @SettingChange -ErrorAction Stop + + if ($Result.success) { + Write-Verbose "Updated Settings successfully." + } + } +} + +function Set-CcmEncryptionPassword { + [CmdletBinding()] + param( + # The CCM server to operate against + [string]$CcmEndpoint = "http://localhost", + + # The current credential for the account to change + [System.Net.NetworkCredential]$Credential = @{ + userName = "ccmadmin" + password = "123qwe" + }, + + # New encryption password to set + [SecureString]$NewPassword, + + # Previous encryption password (unset on fresh install) + [SecureString]$OldPassword = [SecureString]::new() + ) + end { + Update-CcmSettings -CcmEndpoint $CcmEndpoint -Credential $Credential -Settings @{ + encryption = @{ + oldPassphrase = $OldPassword.ToPlainText() + passphrase = $NewPassword.ToPlainText() + confirmPassphrase = $NewPassword.ToPlainText() + } + } + } +} #endregion #region Jenkins Setup @@ -929,12 +1113,12 @@ The host name of the C4B instance. # CCM Values "{{ ccm_fqdn .*?}}" = ([uri]$Data.CCMWebPortal).DnsSafeHost "{{ ccm_port .*?}}" = ([uri]$Data.CCMWebPortal).Port - "{{ ccm_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.DefaultPwToBeChanged) + "{{ ccm_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.CCMCredential.Password.ToPlainText()) # Chocolatey Configuration Values - "{{ ccm_encryption_password .*?}}" = "Requested on first run." - "{{ ccm_client_salt .*?}}" = [System.Web.HttpUtility]::HtmlEncode((Get-ChocoEnvironmentProperty ClientSalt -AsPlainText)) - "{{ ccm_service_salt .*?}}" = [System.Web.HttpUtility]::HtmlEncode((Get-ChocoEnvironmentProperty ServiceSalt -AsPlainText)) + "{{ ccm_encryption_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.CCMEncryptionPassword.ToPlainText()) + "{{ ccm_client_salt .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.ClientSalt.ToPlainText()) + "{{ ccm_service_salt .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.ServiceSalt.ToPlainText()) "{{ chocouser_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.NexusCredential.Password.ToPlainText()) # Nexus Values From f3360c083e6bc15cb2374f3a66dde03ee0389adf Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Mon, 24 Feb 2025 13:03:10 +0000 Subject: [PATCH 06/17] (#291) Removes SSL Unrelated Operations from Set-SSL Customers are being confused by Set-SSL. This change removes remaining non-SSL-related operations from the script, which should allow it to be re-run if required (or accidentally) without issue. --- README.md | 66 +++--- Set-SslSecurity.ps1 | 110 ++-------- Start-C4bCcmSetup.ps1 | 36 ++- Start-C4bJenkinsSetup.ps1 | 15 +- Start-C4bNexusSetup.ps1 | 26 ++- Start-C4bSetup.ps1 | 46 ++-- Start-C4bVerification.ps1 | 4 +- modules/C4B-Environment/C4B-Environment.psm1 | 220 ++++++++++++++----- scripts/Create-ChocoLicensePkg.ps1 | 26 +-- 9 files changed, 320 insertions(+), 229 deletions(-) diff --git a/README.md b/README.md index bb5459a..41eb4c1 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Below are the minimum requirements for setting up your C4B server via this guide 1. If you plan on joining this server to your Active Directory domain, do so now before beginning setup below. -1. If you plan to use a Purchased/Acquired or Domain SSL certificate, please ensure the CN/Subject value matches the DNS-resolvable Fully Qualified Domain Name (FQDN) of your C4B Server. Place this certificate in the `Local Machine > Personal` certificate store, and ensure that the private key is exportable. +1. If you plan to use a Purchased/Acquired or Domain SSL certificate, please ensure the CN/Subject value matches the DNS-resolvable Fully Qualified Domain Name (FQDN) of your C4B Server. Place this certificate in the `Local Machine > Trusted People` certificate store, and ensure that the private key is exportable. 1. Copy your `chocolatey.license.xml` license file (from the email you received) onto your C4B Server. @@ -106,6 +106,34 @@ Below are the minimum requirements for setting up your C4B server via this guide > :memo:**Offline Install**: You can now copy the `C:\choco-setup\` directory to any computer to continue the installation. To zip up that directory, run `Compress-Archive -Path C:\choco-setup\files\* -DestinationPath C:\choco-setup\C4B-Files.zip`. Move the archive to your new machine, and run `Expand-Archive -Path /path/to/C4B-Files.zip -DestinationPath C:\choco-setup\files -Force`. You should then run `Set-Location "$env:SystemDrive\choco-setup\files"; .\Start-C4bSetup.ps1`, and continue with the guide. +#### Running with a Certificate + +**ALTERNATIVE 1 : Custom SSL Certificate** - If you have your own custom SSL certificate (purchased/acquired, or from your Domain CA), you can paste and run the following script with the `Thumbprint` value of your SSL certificate specified: + +```powershell +Set-Location "$env:SystemDrive\choco-setup\files" +.\Start-C4bSetup.ps1 -Thumbprint '' +``` + +> :warning:**REMINDER**: If you are using your own SSL certificate, be sure to place this certificate in the `Local Machine > Personal` certificate store before running the above script, and ensure that the private key is exportable. + +> :memo: **NOTE** +> A Role and User credential will be configured to limit access to your Nexus repositories. As well, CCM Client and Service Salts are configured to further encrypt your connection between CCM and your endpoint clients. These additional settings are also incorporated into your `Register-C4bEndpoint.ps1` script for onboarding endpoints. + +**ALTERNATIVE 2 : Wildcard SSL Certificate** - If you have a wildcard certificate, you will also need to provide a DNS name you wish to use for that certificate: + +```powershell +Set-Location "$env:SystemDrive\choco-setup\files" +.\Start-C4bSetup.ps1 -Thumbprint '' -CertificateDnsName '' +``` + +For example, with a wildcard certificate with a thumbprint of `deee9b2fabb24bdaae71d82286e08de1` you wish to use `chocolatey.foo.org`, the following would be required: + +```powershell +Set-Location "$env:SystemDrive\choco-setup\files" +.\Start-C4bSetup.ps1 -Thumbprint deee9b2fabb24bdaae71d82286e08de1 -CertificateDnsName chocolatey.foo.org +``` + ### Step 2: Nexus Setup 1. In the same **elevated** Windows PowerShell console as above, paste and run the following code: @@ -168,50 +196,20 @@ Below are the minimum requirements for setting up your C4B server via this guide > > -### Step 5: SSL Setup +### Step 5: Complete Setup 1. In the same **elevated** PowerShell console as above, paste and run the following code: ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Set-SslSecurity.ps1 - ``` - - **ALTERNATIVE 1 : Custom SSL Certificate** - If you have your own custom SSL certificate (purchased/acquired, or from your Domain CA), you can paste and run the following script with the `Thumbprint` value of your SSL certificate specified: - - ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Set-SslSecurity.ps1 -Thumbprint '' - ``` - - > :warning:**REMINDER**: If you are using your own SSL certificate, be sure to place this certificate in the `Local Machine > Personal` certificate store before running the above script, and ensure that the private key is exportable. - - > :memo: **NOTE** - > A Role and User credential will be configured to limit access to your Nexus repositories. As well, CCM Client and Service Salts are configured to further encrypt your connection between CCM and your endpoint clients. These additional settings are also incorporated into your `Register-C4bEndpoint.ps1` script for onboarding endpoints. - - **ALTERNATIVE 2 : Wildcard SSL Certificate** - If you have a wildcard certificate, you will also need to provide a DNS name you wish to use for that certificate: - - ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Set-SslSecurity.ps1 -Thumbprint '' -CertificateDnsName '' - ``` - - For example, with a wildcard certificate with a thumbprint of `deee9b2fabb24bdaae71d82286e08de1` you wish to use `chocolatey.foo.org`, the following would be required: - - ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Set-SslSecurity.ps1 -Thumbprint deee9b2fabb24bdaae71d82286e08de1 -CertificateDnsName chocolatey.foo.org + Complete-C4bSetup ``` >
> What does this script do? (click to expand) >
    - >
  • Adds SSL certificate configuration for Nexus and CCM web portals
  • - >
  • Generates a `Register-C4bEndpoint.ps1` script for you to easily set up endpoint clients
  • - >
  • Outputs data to a JSON file to pass between scripts
  • + >
  • Sets up Chocolatey Agent on this system
  • >
  • Writes a Readme.html file to the Public Desktop with account information for C4B services
  • >
  • Auto-opens README, CCM, Nexus, and Jenkins in your web browser
  • - >
  • Removes temporary JSON files used during provisioning
  • >
>
diff --git a/Set-SslSecurity.ps1 b/Set-SslSecurity.ps1 index fa8a667..42a638a 100644 --- a/Set-SslSecurity.ps1 +++ b/Set-SslSecurity.ps1 @@ -31,11 +31,16 @@ param( ) } })] + [ValidateScript({Test-CertificateDomain -Thumbprint $_})] [string] $Thumbprint = $( - Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { - $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed - } | Select-Object -ExpandProperty Thumbprint -First 1 + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint + } else { + Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { + $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed + } | Select-Object -ExpandProperty Thumbprint -First 1 + } ), # The certificate subject that identifies the target SSL certificate in @@ -57,10 +62,7 @@ param( [string]$NuGetApiKey = $( if (-not (Get-Command Get-ChocoEnvironmentProperty -ErrorAction SilentlyContinue)) {. $PSScriptRoot\scripts\Get-Helpers.ps1} Get-ChocoEnvironmentProperty NuGetApiKey -AsPlainText - ), - - # If provided, will skip launching the browser - [switch]$SkipBrowserLaunch + ) ) process { $DefaultEap = $ErrorActionPreference @@ -74,18 +76,8 @@ process { Get-Certificate -Thumbprint $Thumbprint } - if (-not $CertificateDnsName) { - $matcher = 'CN\s?=\s?(?[^,\s]+)' - $null = $Certificate.Subject -match $matcher - $SubjectWithoutCn = if ($Matches.Subject.StartsWith('*')) { - # This is a wildcard cert, we need to prompt for the intended CertificateDnsName - while ($CertificateDnsName -notlike $Matches.Subject) { - $CertificateDnsName = Read-Host -Prompt "$(if ($CertificateDnsName) {"'$($CertificateDnsName)' is not a subdomain of '$($Matches.Subject)'. "})Please provide an FQDN to use with the certificate '$($Matches.Subject)'" - } - $CertificateDnsName - } else { - $Matches.Subject - } + if (-not $CertificateDnsName -and -not ($CertificateDnsName = Get-ChocoEnvironmentProperty CertSubject)) { + $null = Test-CertificateDomain -Thumbprint $Certificate.Thumbprint } else { $SubjectWithoutCn = $CertificateDnsName } @@ -107,7 +99,7 @@ process { Start-Service nexus Write-Warning "Waiting to give Nexus time to start up on 'https://${SubjectWithoutCn}:8443'" - Wait-Nexus + Wait-Site Nexus # Build Credential Object, Connect to Nexus if ($Credential = Get-ChocoEnvironmentProperty NexusCredential) { @@ -122,8 +114,8 @@ process { # Push ClientSetup.ps1 to raw repo $ClientScript = "$PSScriptRoot\scripts\ClientSetup.ps1" - (Get-Content -Path $ClientScript) -replace "{{hostname}}", $SubjectWithoutCn | Set-Content -Path $ClientScript - $null = New-NexusRawComponent -RepositoryName 'choco-install' -File $ClientScript + (Get-Content -Path $ClientScript) -replace "{{hostname}}", "$((Get-NexusLocalServiceUri) -replace '^https?:\/\/')" | Set-Content -Path ($TemporaryFile = New-TemporaryFile).FullName + $null = New-NexusRawComponent -RepositoryName 'choco-install' -File $TemporaryFile.FullName -Name "ClientSetup.ps1" $NexusPw = Get-ChocoEnvironmentProperty ChocoUserPassword -AsPlainText @@ -199,34 +191,18 @@ process { # Generate Register-C4bEndpoint.ps1 $EndpointScript = "$PSScriptRoot\scripts\Register-C4bEndpoint.ps1" - $ClientSaltValue = Get-ChocoEnvironmentProperty ClientSalt -AsPlainText - $ServiceSaltValue = Get-ChocoEnvironmentProperty ServiceSalt -AsPlainText Invoke-TextReplacementInFile -Path $EndpointScript -Replacement @{ - "{{ ClientSaltValue }}" = $ClientSaltValue - "{{ ServiceSaltValue }}" = $ServiceSaltValue + "{{ ClientSaltValue }}" = Get-ChocoEnvironmentProperty ClientSalt -AsPlainText + "{{ ServiceSaltValue }}" = Get-ChocoEnvironmentProperty ServiceSalt -AsPlainText "{{ FQDN }}" = $SubjectWithoutCn - } - - # Agent Setup - $agentArgs = @{ - CentralManagementServiceUrl = "https://$($SubjectWithoutCn):24020/ChocolateyManagementService" - ServiceSalt = $ServiceSaltValue - ClientSalt = $ClientSaltValue - } - if (Test-SelfSignedCertificate -Certificate $Certificate) { - # Register endpoint script - (Get-Content -Path $EndpointScript) -replace "{{hostname}}", "'$SubjectWithoutCn'" | Set-Content -Path $EndpointScript - $ScriptBlock = @" -`$downloader = New-Object -TypeName System.Net.WebClient -Invoke-Expression (`$downloader.DownloadString("http://`$(`$HostName):80/Import-ChocoServerCertificate.ps1")) -"@ - (Get-Content -Path $EndpointScript) -replace "# placeholder if using a self-signed cert", $ScriptBlock | Set-Content -Path $EndpointScript + # Set a default value for TrustCertificate if we're using a self-signed cert + '(?\s+\$TrustCertificate)(?\s*=\s*\$true)?(?,)?(?!\))' = "`${Parameter}$( + if (Test-SelfSignedCertificate -Certificate $Certificate) {' = $true'} + )`${Comma}" } - Install-ChocolateyAgent @agentArgs - Update-Clixml -Properties @{ CCMWebPortal = "https://$($SubjectWithoutCn)/Account/Login" CCMServiceURL = "https://$($SubjectWithoutCn):24020/ChocolateyManagementService" @@ -234,51 +210,11 @@ Invoke-Expression (`$downloader.DownloadString("http://`$(`$HostName):80/Import- CertThumbprint = $Certificate.Thumbprint CertExpiry = $Certificate.NotAfter IsSelfSigned = $IsSelfSigned - ServiceSalt = ConvertTo-SecureString $ServiceSaltValue -AsPlainText -Force - ClientSalt = ConvertTo-SecureString $ClientSaltValue -AsPlainText -Force } } end { - Write-Host 'Writing README to Desktop; this file contains login information for all C4B services.' - New-QuickstartReadme - - if (-not $SkipBrowserLaunch -and $Host.Name -eq 'ConsoleHost') { - $Message = 'The CCM, Nexus & Jenkins sites will open in your browser in 10 seconds. Press any key to skip this.' - $Timeout = New-TimeSpan -Seconds 10 - $Stopwatch = [System.Diagnostics.Stopwatch]::new() - $Stopwatch.Start() - Write-Host $Message -NoNewline -ForegroundColor Green - do { - # wait for a key to be available: - if ([Console]::KeyAvailable) { - # read the key, and consume it so it won't - # be echoed to the console: - $keyInfo = [Console]::ReadKey($true) - Write-Host "`nSkipping the Opening of sites in your browser." -ForegroundColor Green - # exit loop - break - } - # write a dot and wait a second - Write-Host '.' -NoNewline -ForegroundColor Green - Start-Sleep -Seconds 1 - } - while ($Stopwatch.Elapsed -lt $Timeout) - $Stopwatch.Stop() - - if (-not ($keyInfo)) { - Write-Host "`nOpening CCM, Nexus & Jenkins sites in your browser." -ForegroundColor Green - $Readme = 'file:///C:/Users/Public/Desktop/README.html' - $Ccm = "https://$($SubjectWithoutCn)/Account/Login" - $Nexus = "https://$($SubjectWithoutCn):8443" - $Jenkins = "https://$($SubjectWithoutCn):7443" - try { - Start-Process msedge.exe "$Readme", "$Ccm", "$Nexus", "$Jenkins" - } catch { - Start-Process chrome.exe "$Readme", "$Ccm", "$Nexus", "$Jenkins" - } - } - } - $ErrorActionPreference = $DefaultEap Stop-Transcript -} + + Complete-C4bSetup -SkipBrowserLaunch +} \ No newline at end of file diff --git a/Start-C4bCcmSetup.ps1 b/Start-C4bCcmSetup.ps1 index bf1fcee..4b37cc6 100644 --- a/Start-C4bCcmSetup.ps1 +++ b/Start-C4bCcmSetup.ps1 @@ -15,7 +15,7 @@ param( [Parameter()] [ValidateNotNull()] [System.Management.Automation.PSCredential] - $DatabaseCredential = ( + $DatabaseCredential = $( if ($DatabaseCredential = Get-ChocoEnvironmentProperty DatabaseUser) { $DatabaseCredential } else { @@ -36,8 +36,17 @@ param( ) } })] + [ValidateScript({Test-CertificateDomain -Thumbprint $_})] [String] - $Thumbprint + $Thumbprint = $( + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint + } else { + Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { + $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed + } | Select-Object -ExpandProperty Thumbprint -First 1 + } + ) ) process { $DefaultEap = $ErrorActionPreference @@ -122,7 +131,8 @@ process { & Invoke-Choco @chocoArgs Write-Host "Creating Chocolatey Central Management Database" - choco install chocolatey-management-database --source='ChocolateyInternal' -y --package-parameters="'/ConnectionString=Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;Trusted_Connection=true;'" --no-progress + $chocoArgs = @('install', 'chocolatey-management-database', '--source="ChocolateyInternal"', '-y', '--package-parameters="''/ConnectionString=Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;Trusted_Connection=true;''"', '--no-progress') + & Invoke-Choco @chocoArgs # Add Local Windows User: $DatabaseUser = $DatabaseCredential.UserName @@ -155,6 +165,8 @@ process { $chocoArgs += @("--package-parameters='/CertificateThumbprint=$Thumbprint'") } & Invoke-Choco @chocoArgs + + if (-not $MyCertificate) {$MyCertificate = Get-Item Cert:\LocalMachine\My\*} Write-Host "Installing Chocolatey Central Management Website" $chocoArgs = @('install', 'chocolatey-management-web', "--source='ChocolateyInternal'", '-y', "--package-parameters-sensitive=""'/ConnectionString:Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;User ID=$DatabaseUser;Password=$DatabaseUserPw;'""", '--no-progress') @@ -169,12 +181,30 @@ process { $CcmEndpoint = "https://$(Get-ChocoEnvironmentProperty CertSubject)" } + choco config set centralManagementServiceUrl "$($CcmEndpoint):24020/ChocolateyManagementService" + + # Updating the Registration Script + $EndpointScript = "$PSScriptRoot\scripts\Register-C4bEndpoint.ps1" + Invoke-TextReplacementInFile -Path $EndpointScript -Replacement @{ + "{{ ClientSaltValue }}" = Get-ChocoEnvironmentProperty ClientSalt -AsPlainText + "{{ ServiceSaltValue }}" = Get-ChocoEnvironmentProperty ServiceSalt -AsPlainText + "{{ FQDN }}" = Get-ChocoEnvironmentProperty CertSubject + + # Set a default value for TrustCertificate if we're using a self-signed cert + '(?\s+\$TrustCertificate)(?\s*=\s*\$true)?(?,)?(?!\))' = "`${Parameter}$( + if (Test-SelfSignedCertificate -Certificate $MyCertificate) {' = $true'} + )`${Comma}" + } # Create the site hosting the certificate import script on port 80 if ($MyCertificate.NotAfter -gt (Get-Date).AddYears(5)) { .\scripts\New-IISCertificateHost.ps1 } + Wait-Site CCM + + Write-Host "Configuring Chocolatey Central Management" + # Run initial configuration for CCM Admin if (-not ($CCMCredential = Get-ChocoEnvironmentProperty CCMCredential)) { $CCMCredential = [PSCredential]::new( diff --git a/Start-C4bJenkinsSetup.ps1 b/Start-C4bJenkinsSetup.ps1 index b46448c..0f5a668 100644 --- a/Start-C4bJenkinsSetup.ps1 +++ b/Start-C4bJenkinsSetup.ps1 @@ -33,11 +33,16 @@ param( ) } })] + [ValidateScript({Test-CertificateDomain -Thumbprint $_})] [string] $Thumbprint = $( - Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { - $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed - } | Select-Object -ExpandProperty Thumbprint -First 1 + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint + } else { + Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { + $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed + } | Select-Object -ExpandProperty Thumbprint -First 1 + } ) ) process { @@ -59,8 +64,8 @@ process { $chocoArgs = @('install', 'jenkins', "--source='ChocolateyInternal'", '-y', '--no-progress') & Invoke-Choco @chocoArgs - Write-Host "Giving Jenkins 30 seconds to complete background setup..." -ForegroundColor Green - Start-Sleep -Seconds 30 # Jenkins needs a moment + # Jenkins needs a moment + Wait-Site Jenkins # Disabling inital setup prompts $JenkinsHome = "C:\ProgramData\Jenkins\.jenkins" diff --git a/Start-C4bNexusSetup.ps1 b/Start-C4bNexusSetup.ps1 index 55b3796..6f3aa73 100644 --- a/Start-C4bNexusSetup.ps1 +++ b/Start-C4bNexusSetup.ps1 @@ -29,8 +29,17 @@ param( ) } })] + [ValidateScript({Test-CertificateDomain -Thumbprint $_})] [string] - $Thumbprint + $Thumbprint = $( + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint + } else { + Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { + $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed + } | Select-Object -ExpandProperty Thumbprint -First 1 + } + ) ) process { $DefaultEap = $ErrorActionPreference @@ -52,11 +61,10 @@ process { $NexusPort = 8443 $null = Set-NexusCert -Thumbprint $Thumbprint -Port $NexusPort - - Import-Module NexuShell if ($CertificateDnsName = Get-ChocoEnvironmentProperty CertSubject) { - & (Get-Module NexuShell) {Get-NexusUri -HostnameOverride $CertificateDnsName} | Write-Verbose + # Override the domain, so we don't get prompted for wildcard certificates + Get-NexusLocalServiceUri -HostnameOverride $CertificateDnsName | Write-Verbose } } @@ -70,7 +78,7 @@ process { } $null = New-NetFirewallRule @FwRuleParams - Wait-Nexus + Wait-Site Nexus Write-Host "Configuring Sonatype Nexus Repository" @@ -227,6 +235,11 @@ process { Write-Error "ChocolateyInstall.ps1 script signature is not valid. Please investigate." } + # Push ClientSetup.ps1 to raw repo + $ClientScript = "$PSScriptRoot\scripts\ClientSetup.ps1" + (Get-Content -Path $ClientScript) -replace "{{hostname}}", "$((Get-NexusLocalServiceUri) -replace '^https?:\/\/')" | Set-Content -Path ($TemporaryFile = New-TemporaryFile).FullName + $null = New-NexusRawComponent -RepositoryName 'choco-install' -File $TemporaryFile.FullName -Name "ClientSetup.ps1" + # Nexus NuGet V3 Compatibility Invoke-Choco feature disable --name="'usePackageRepositoryOptimizations'" @@ -239,6 +252,7 @@ process { Invoke-Choco source disable -n 'ChocolateyTest' # Push all packages from previous steps to NuGet repo + Write-Host "Pushing C4B Environment Packages to ChocolateyInternal" Get-ChildItem -Path "$env:SystemDrive\choco-setup\files\files" -Filter *.nupkg | ForEach-Object { Invoke-Choco push $_.FullName --source $LocalSource --apikey $NugetApiKey --force } @@ -270,7 +284,7 @@ process { # Save useful params Update-Clixml -Properties @{ - NexusUri = & (Get-Module NexuShell) {Get-NexusUri} + NexusUri = Get-NexusLocalServiceUri NexusCredential = $Credential NexusRepo = "$((Get-NexusRepository -Name 'ChocolateyInternal').url)/index.json" NuGetApiKey = $NugetApiKey | ConvertTo-SecureString -AsPlainText -Force diff --git a/Start-C4bSetup.ps1 b/Start-C4bSetup.ps1 index 013a342..030681e 100644 --- a/Start-C4bSetup.ps1 +++ b/Start-C4bSetup.ps1 @@ -52,8 +52,8 @@ param( [Parameter(ParameterSetName='Unattended')] [System.Management.Automation.PSCredential] $DatabaseCredential = $( - if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseCredential) { - (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseCredential + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseUser) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseUser } elseif ($PSCmdlet.ParameterSetName -eq 'Unattended') { $Wshell = New-Object -ComObject Wscript.Shell $null = $Wshell.Popup('You will now create a credential for the ChocolateyManagement DB user, to be used by CCM (document this somewhere).') @@ -76,7 +76,21 @@ param( } })] [string] - $Thumbprint, + $Thumbprint = $( + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint + } else { + Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { + $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed + } | Select-Object -ExpandProperty Thumbprint -First 1 + } + ), + + # If using a wildcard certificate, provide a DNS name you want to use to access services secured by the certificate.\ + [string]$CertificateDnsName = $( + if (-not (Get-Command Get-ChocoEnvironmentProperty -ErrorAction SilentlyContinue)) {. $PSScriptRoot\scripts\Get-Helpers.ps1} + Get-ChocoEnvironmentProperty CertSubject + ), # If provided, shows all Chocolatey output. Otherwise, blissful quiet. [switch]$ShowChocoOutput, @@ -85,7 +99,10 @@ param( # Defaults to main. [string] [Alias('PR')] - $Branch = $env:CHOCO_QSG_BRANCH + $Branch = $env:CHOCO_QSG_BRANCH, + + # If provided, will skip launching the browser + [switch]$SkipBrowserLaunch ) if ($ShowChocoOutput) { $global:PSDefaultParameterValues["Invoke-Choco:InformationAction"] = "Continue" @@ -151,20 +168,10 @@ try { $Certificate = Get-Certificate -Thumbprint $Thumbprint Copy-CertToStore -Certificate $Certificate - if (-not ($CertificateDnsName = Get-ChocoEnvironmentProperty CertSubject)) { - $matcher = 'CN\s?=\s?(?[^,\s]+)' - $null = $Certificate.Subject -match $matcher - $CertificateDnsName = if ($Matches.Subject.StartsWith('*')) { - # This is a wildcard cert, we need to prompt for the intended CertificateDnsName - while ($CertificateDnsName -notlike $Matches.Subject) { - $CertificateDnsName = Read-Host -Prompt "$(if ($CertificateDnsName) {"'$($CertificateDnsName)' is not a subdomain of '$($Matches.Subject)'. "})Please provide an FQDN to use with the certificate '$($Matches.Subject)'" - } - $CertificateDnsName - } else { - $Matches.Subject - } - Set-ChocoEnvironmentProperty CertSubject $CertificateDnsName - } + $null = Test-CertificateDomain -Thumbprint $Thumbprint + } elseif ($PSScriptRoot) { + # We're going to be using a self-signed certificate + Set-ChocoEnvironmentProperty CertSubject $env:ComputerName } if ($DatabaseCredential) { @@ -200,7 +207,8 @@ try { .\Start-C4BNexusSetup.ps1 @Certificate .\Start-C4bCcmSetup.ps1 @Certificate -DatabaseCredential $DatabaseCredential .\Start-C4bJenkinsSetup.ps1 @Certificate - .\Set-SslSecurity.ps1 @Certificate + + Complete-C4bSetup -SkipBrowserLaunch:$SkipBrowserLaunch } } finally { $ErrorActionPreference = $DefaultEap diff --git a/Start-C4bVerification.ps1 b/Start-C4bVerification.ps1 index 639c4f6..c4a5de1 100644 --- a/Start-C4bVerification.ps1 +++ b/Start-C4bVerification.ps1 @@ -1,9 +1,9 @@ #requires -modules C4B-Environment [CmdletBinding()] Param( - [Parameter(Mandatory)] + [Parameter()] [String] - $Fqdn + $Fqdn = $(Get-ChocoEnvironmentProperty CertSubject) ) process { if (-not (Get-Module Pester -ListAvailable).Where{$_.Version -gt "5.0"}) { diff --git a/modules/C4B-Environment/C4B-Environment.psm1 b/modules/C4B-Environment/C4B-Environment.psm1 index 31d03ae..ca32a41 100644 --- a/modules/C4B-Environment/C4B-Environment.psm1 +++ b/modules/C4B-Environment/C4B-Environment.psm1 @@ -39,6 +39,129 @@ function Invoke-Choco { } } +function Test-CertificateDomain { + param( + [Parameter(Mandatory)] + [string]$Thumbprint + ) + # Check the certificate exists + if (-not ($Certificate = Get-Item Cert:\LocalMachine\TrustedPeople\$Thumbprint)) { + throw "Certificate could not be found in Cert:\LocalMachine\TrustedPeople\. Please ensure it is is present, and try again." + } + + # Check that we have a domain for it + if (-not ($CertificateDnsName = Get-ChocoEnvironmentProperty CertSubject) -and ($Certificate.Subject -match '^CN=\*')) { + $matcher = 'CN\s?=\s?(?[^,\s]+)' + $null = $Certificate.Subject -match $matcher + $CertificateDnsName = if ($Matches.Subject.StartsWith('*')) { + # This is a wildcard cert, we need to prompt for the intended CertificateDnsName + while ($CertificateDnsName -notlike $Matches.Subject) { + $CertificateDnsName = Read-Host -Prompt "$(if ($CertificateDnsName) {"'$($CertificateDnsName)' is not a subdomain of '$($Matches.Subject)'. "})Please provide an FQDN to use with the certificate '$($Matches.Subject)'" + } + $CertificateDnsName + } else { + $Matches.Subject + } + Set-ChocoEnvironmentProperty CertSubject $CertificateDnsName + } + + $true +} + +function Wait-Site { + <# + .Synopsis + Waits for a given site to be available. A simple healthcheck. + #> + [Alias('Wait-Nexus','Wait-CCM','Wait-Jenkins')] + [CmdletBinding(DefaultParameterSetName="Name")] + param( + # The service name to check for a 200 response + [Parameter(ParameterSetName='Name', Position=0)] + [ValidateSet('Nexus','CCM','Jenkins')] + [string]$Name = $MyInvocation.InvocationName.Split('-')[-1], + + # The Url to check for a 200 response + [Parameter(ParameterSetName='Url', Mandatory, Position=0)] + [string]$Url = @{ + 'Nexus' = { + try { + Get-NexusLocalServiceUri + } catch { + Write-Verbose "Nexus may not be installed yet." + "http://localhost:8081" + } + } + 'CCM' = { + try { + $Binding = Get-WebBinding -Name ChocolateyCentralManagement + $Domain = if ( + $Binding.protocol -eq 'https' -and + ($Certificate = Get-ChildItem Cert:\LocalMachine\TrustedPeople | Where-Object Subject -notlike 'CN=`**').Count -eq 1 -and + $Certificate.Subject -match "^CN=(?.+)(?:,|$)" + ) { + $Matches.Domain + } elseif ($Binding.protocol -eq 'https' -and ($CertSubject = Get-ChocoEnvironmentProperty CertSubject)) { + $CertSubject + } else { + 'localhost' + } + "$($Binding.protocol)://$($Domain):$($Binding.bindingInformation.Trim('*').Trim(':'))/" + } catch { + Write-Verbose "CCM may not be installed yet." + "http://localhost" + } + } + 'Jenkins' = { + try { + if (Test-Path "C:\Program Files\Jenkins\jenkins.xml") { + [xml]$Xml = Get-Content "C:\Program Files\Jenkins\jenkins.xml" + if ($Xml.SelectSingleNode("/service/arguments").'#text' -match "--(?https?)Port=(?\d+)\b") { + $Port = $Matches.PortNumber + $Scheme = $Matches.Scheme + } + $Domain = if ($Scheme -eq 'https') { + Get-ChocoEnvironmentProperty CertSubject + } else { + 'localhost' + } + "$($Scheme)://$($Domain):$($Port)/login" # TODO: Get PATH + } elseif (Test-Path "C:\Program Files\Jenkins\jenkins.model.JenkinsLocationConfiguration.xml") { + [xml]$Location = (Get-Content "C:\Program Files\Jenkins\jenkins.model.JenkinsLocationConfiguration.xml" -ErrorAction Stop) -replace "^\<\?xml version=['""]1\.1['""]"," @@ -133,9 +127,9 @@ If items are installed in any other order, it could have strange effects or fail "@.Trim() | Set-Content -Path "$licensePackageNuspec" -Encoding UTF8 -Force # Package up everything -Write-Host "Creating license package..." -choco pack $licensePackageNuspec --output-directory="$PackagesPath" -Write-Host "Package has been created and is ready at $PackagesPath" +Write-Verbose "Creating license package..." +Invoke-Choco pack $licensePackageNuspec --output-directory="$PackagesPath" +Write-Verbose "Package has been created and is ready at $PackagesPath" -Write-Host "Installing newly created package on this machine, making updates to license easier in the future, if pushed from another location later." -choco upgrade chocolatey-license -y --source="'$PackagesPath'" +Write-Verbose "Installing newly created package on this machine, making updates to license easier in the future, if pushed from another location later." +Invoke-Choco upgrade chocolatey-license -y --source="'$PackagesPath'" From 94afea4c000a60812d333c43deac4daed6cf5362 Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Tue, 25 Feb 2025 11:02:30 +0000 Subject: [PATCH 07/17] (#290) Removes usage of Web::GeneratePassword This generator has no control over which characters are used and so was creating issues with our lazy replacements in the readme. This removes all uses of it in favour of the method used in New-ServicePassword, which is more controlled. Consequently, this commit also removes the New-CCMSalt function, which is now unused. --- Start-C4bCcmSetup.ps1 | 12 ++++++------ Start-C4bNexusSetup.ps1 | 3 +-- modules/C4B-Environment/C4B-Environment.psm1 | 15 --------------- 3 files changed, 7 insertions(+), 23 deletions(-) diff --git a/Start-C4bCcmSetup.ps1 b/Start-C4bCcmSetup.ps1 index 4b37cc6..c8cdb99 100644 --- a/Start-C4bCcmSetup.ps1 +++ b/Start-C4bCcmSetup.ps1 @@ -224,17 +224,17 @@ process { # Set Client and Service salts if (-not (Get-ChocoEnvironmentProperty ClientSalt)) { - $ClientSaltValue = New-CCMSalt - Set-ChocoEnvironmentProperty ClientSalt (ConvertTo-SecureString $ClientSaltValue -AsPlainText -Force) + $ClientSaltValue = New-ServicePassword + Set-ChocoEnvironmentProperty ClientSalt $ClientSaltValue - Invoke-Choco config set centralManagementClientCommunicationSaltAdditivePassword $ClientSaltValue + Invoke-Choco config set centralManagementClientCommunicationSaltAdditivePassword $ClientSaltValue.ToPlainText() } if (-not (Get-ChocoEnvironmentProperty ServiceSalt)) { - $ServiceSaltValue = New-CCMSalt - Set-ChocoEnvironmentProperty ServiceSalt (ConvertTo-SecureString $ServiceSaltValue -AsPlainText -Force) + $ServiceSaltValue = New-ServicePassword + Set-ChocoEnvironmentProperty ServiceSalt $ServiceSaltValue - Invoke-Choco config set centralManagementServiceCommunicationSaltAdditivePassword $ServiceSaltValue + Invoke-Choco config set centralManagementServiceCommunicationSaltAdditivePassword $ServiceSaltValue.ToPlainText() } $CcmSvcUrl = Invoke-Choco config get centralManagementServiceUrl -r diff --git a/Start-C4bNexusSetup.ps1 b/Start-C4bNexusSetup.ps1 index 6f3aa73..d1676b1 100644 --- a/Start-C4bNexusSetup.ps1 +++ b/Start-C4bNexusSetup.ps1 @@ -156,11 +156,10 @@ process { # Create new user for endpoints if (-not (Get-NexusUser -User 'chocouser' -ErrorAction SilentlyContinue)) { - $NexusPw = [System.Web.Security.Membership]::GeneratePassword(32, 12) # Create Nexus user $UserParams = @{ Username = 'chocouser' - Password = ($NexusPw | ConvertTo-SecureString -AsPlainText -Force) + Password = New-ServicePassword FirstName = 'Choco' LastName = 'User' EmailAddress = 'chocouser@example.com' diff --git a/modules/C4B-Environment/C4B-Environment.psm1 b/modules/C4B-Environment/C4B-Environment.psm1 index ca32a41..7a7948e 100644 --- a/modules/C4B-Environment/C4B-Environment.psm1 +++ b/modules/C4B-Environment/C4B-Environment.psm1 @@ -573,21 +573,6 @@ ALTER ROLE [$DatabaseRole] ADD MEMBER [$Username] $Connection.Close() } -function New-CcmSalt { - [CmdletBinding()] - param( - [Parameter()] - [int] - $MinLength = 32, - [Parameter()] - [int] - $SpecialCharCount = 12 - ) - process { - [System.Web.Security.Membership]::GeneratePassword($MinLength, $SpecialCharCount) - } -} - function Stop-CCMService { #Stop Central Management components Stop-Service chocolatey-central-management From 395e15e98c90162f411888d09b3ac7810ae104d4 Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Thu, 27 Feb 2025 12:27:06 +0000 Subject: [PATCH 08/17] (#291) Updates ClientSetup to match other Environments --- scripts/ClientSetup.ps1 | 106 +++++++++++++++---------------- scripts/Register-C4bEndpoint.ps1 | 3 +- 2 files changed, 54 insertions(+), 55 deletions(-) diff --git a/scripts/ClientSetup.ps1 b/scripts/ClientSetup.ps1 index ed1b176..3b0f6fa 100644 --- a/scripts/ClientSetup.ps1 +++ b/scripts/ClientSetup.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS -Completes client setup for a client machine to communicate with the C4B Server. +Completes client setup for a client machine to communicate with CCM. #> [CmdletBinding(DefaultParameterSetName = 'Default')] param( @@ -9,12 +9,11 @@ param( [Parameter()] [Alias('Url')] [string] - $RepositoryUrl = 'https://{{hostname}}:8443/repository/ChocolateyInternal/index.json', + $RepositoryUrl = 'https://{{hostname}}/repository/ChocolateyInternal/index.json', - # The credential necessary to access the internal Nexus repository. This can - # be ignored if Anonymous authentication is enabled. - # This parameter will be necessary if your C4B server is web-enabled. + # The credential used to access the internal Nexus repository. [Parameter(Mandatory)] + [Alias('Credential')] [pscredential] $RepositoryCredential, @@ -42,16 +41,18 @@ param( # Client salt value used to populate the centralManagementClientCommunicationSaltAdditivePassword # value in the Chocolatey config file [Parameter()] + [Alias('ClientSalt')] [string] $ClientCommunicationSalt, # Server salt value used to populate the centralManagementServiceCommunicationSaltAdditivePassword # value in the Chocolatey config file [Parameter()] + [Alias('ServerSalt')] [string] $ServiceCommunicationSalt, - #Install the Chocolatey Licensed Extension with right-click context menus available + # Install the Chocolatey Licensed Extension with right-click context menus available [Parameter()] [Switch] $IncludePackageTools, @@ -62,7 +63,7 @@ param( [Hashtable] $AdditionalConfiguration, - # Allows for the toggling of additonal features that is applied after the base configuration. + # Allows for the toggling of additional features that is applied after the base configuration. # Can override base configuration with this parameter [Parameter()] [Hashtable] @@ -82,8 +83,7 @@ param( Set-ExecutionPolicy Bypass -Scope Process -Force -$hostAddress = $RepositoryUrl.Split('/')[2] -$hostName = ($hostAddress -split ':')[0] +$hostName = ([uri]$RepositoryUrl).DnsSafeHost $params = @{ ChocolateyVersion = $ChocolateyVersion @@ -91,16 +91,19 @@ $params = @{ UseNativeUnzip = $true } -if (-not $IgnoreProxy) { - if ($ProxyUrl) { - $proxy = [System.Net.WebProxy]::new($ProxyUrl, $true <#bypass on local#>) - $params.Add('ProxyUrl', $ProxyUrl) - } +if (-not $IgnoreProxy -and $ProxyUrl) { + $Proxy = [System.Net.WebProxy]::new( + $ProxyUrl, + $true # Bypass Local Addresses + ) + $params.Add('ProxyUrl', $ProxyUrl) if ($ProxyCredential) { + $Proxy.Credentials = $ProxyCredential $params.Add('ProxyCredential', $ProxyCredential) - $proxy.Credentials = $ProxyCredential - + } elseif ($DefaultProxyCredential = [System.Net.CredentialCache]::DefaultCredentials) { + $Proxy.Credentials = $DefaultProxyCredential + $params.Add('ProxyCredential', $DefaultProxyCredential) } } @@ -114,23 +117,22 @@ $NupkgUrl = if (-not $ChocolateyVersion) { $QueryUrl = (($RepositoryUrl -replace '/index\.json$'), "v3/registration/Chocolatey/index.json") -join '/' $Result = $webClient.DownloadString($QueryUrl) | ConvertFrom-Json $Result.items.items[-1].packageContent -} -else { +} else { # Otherwise, assume the URL "$($RepositoryUrl -replace '/index\.json$')/v3/content/chocolatey/$($ChocolateyVersion)/chocolatey.$($ChocolateyVersion).nupkg" } +$webClient.Proxy = if ($Proxy -and -not $Proxy.IsBypassed($NupkgUrl)) {$Proxy} + # Download the NUPKG $NupkgPath = Join-Path $env:TEMP "$(New-Guid).zip" $webClient.DownloadFile($NupkgUrl, $NupkgPath) # Add Parameter for ChocolateyDownloadUrl, that is the NUPKG path $params.Add('ChocolateyDownloadUrl', $NupkgPath) - -# Get the script content -$script = $webClient.DownloadString("https://${hostAddress}/repository/choco-install/ChocolateyInstall.ps1") - -# Run the Chocolatey Install script with the parameters provided +$InstallScriptUrl = $RepositoryUrl -replace '\/repository\/(?.+)\/(index.json)?$', '/repository/choco-install/ChocolateyInstall.ps1' +$webClient.Proxy = if ($Proxy -and -not $Proxy.IsBypassed($InstallScriptUrl)) {$Proxy} +$script = $webClient.DownloadString($InstallScriptUrl) & ([scriptblock]::Create($script)) @params # If FIPS is enabled, configure Chocolatey to use FIPS compliant checksums @@ -146,23 +148,24 @@ choco config set commandExecutionTimeoutSeconds 14400 # Nexus NuGet V3 Compatibility choco feature disable --name="'usePackageRepositoryOptimizations'" -# Environment base Source configuration choco source add --name="'ChocolateyInternal'" --source="'$RepositoryUrl'" --allow-self-service --user="'$($RepositoryCredential.UserName)'" --password="'$($RepositoryCredential.GetNetworkCredential().Password)'" --priority=1 + choco source disable --name="'Chocolatey'" choco source disable --name="'chocolatey.licensed'" -choco upgrade chocolatey-license -y --source="'ChocolateyInternal'" -if (-not $IncludePackageTools) { - choco upgrade chocolatey.extension -y --params="'/NoContextMenu'" --source="'ChocolateyInternal'" --no-progress -} -else { - Write-Warning "IncludePackageTools was passed. Right-Click context menus will be available for installers, .nupkg, and .nuspec file types!" - choco upgrade chocolatey.extension -y --source="'ChocolateyInternal'" --no-progress -} -choco upgrade chocolateygui -y --source="'ChocolateyInternal'" --no-progress -choco upgrade chocolateygui.extension -y --source="'ChocolateyInternal'" --no-progress +choco upgrade chocolatey-license --confirm --source="'ChocolateyInternal'" +choco upgrade chocolatey.extension --confirm --source="'ChocolateyInternal'" --no-progress @( + if (-not $IncludePackageTools) { + '--params="/NoContextMenu"' + } else { + Write-Verbose "IncludePackageTools was passed. Right-Click context menus will be available for installers, .nupkg, and .nuspec file types!" + } +) -choco upgrade chocolatey-agent -y --source="'ChocolateyInternal'" +choco upgrade chocolateygui --confirm --source="'ChocolateyInternal'" --no-progress +choco upgrade chocolateygui.extension --confirm --source="'ChocolateyInternal'" --no-progress + +choco upgrade chocolatey-agent --confirm --source="'ChocolateyInternal'" # Chocolatey Package Upgrade Resilience choco feature enable --name="'excludeChocolateyPackagesDuringUpgradeAll'" @@ -188,20 +191,19 @@ if ($ServiceCommunicationSalt) { choco feature enable --name="'useChocolateyCentralManagement'" choco feature enable --name="'useChocolateyCentralManagementDeployments'" - if ($AdditionalConfiguration -or $AdditionalFeatures -or $AdditionalSources -or $AdditionalPackages) { - Write-Host "Applying user supplied configuration" -ForegroundColor Cyan + Write-Host "Applying user supplied configuration" } -# How we call choco from here changes as we need to be more dynamic with thingsii . + if ($AdditionalConfiguration) { - <# +<# We expect to pass in a hashtable with configuration information with the following shape: @{ - Name = BackgroundServiceAllowedCommands - Value = 'install,upgrade,uninstall' + BackgroundServiceAllowedCommands = 'install,upgrade,uninstall' + commandExecutionTimeoutSeconds = 6000 } - #> +#> $AdditionalConfiguration.GetEnumerator() | ForEach-Object { $Config = [System.Collections.Generic.list[string]]::new() @@ -215,15 +217,14 @@ if ($AdditionalConfiguration) { } if ($AdditionalFeatures) { - <# +<# We expect to pass in feature information as a hashtable with the following shape: @{ useBackgroundservice = 'Enabled' } - #> +#> $AdditionalFeatures.GetEnumerator() | ForEach-Object { - $Feature = [System.Collections.Generic.list[string]]::new() $Feature.Add('feature') @@ -240,13 +241,12 @@ if ($AdditionalFeatures) { } if ($AdditionalSources) { - - <# - We expect a user to pass in a hashtable with source information with the folllowing shape: +<# + We expect a user to pass in a hashtable with source information with the following shape: @{ Name = 'MySource' Source = 'https://nexus.fabrikam.com/repository/MyChocolateySource' - #Optional items + # Optional items Credentials = $MySourceCredential AllowSelfService = $true AdminOnly = $true @@ -256,7 +256,7 @@ if ($AdditionalSources) { CertificatePassword = 's0mepa$$' } #> - Foreach ($Source in $AdditionalSources) { + foreach ($Source in $AdditionalSources) { $SourceSplat = [System.Collections.Generic.List[string]]::new() # Required items $SourceSplat.Add('source') @@ -284,8 +284,7 @@ if ($AdditionalSources) { } if ($AdditionalPackages) { - - <# +<# We expect to pass in a hashtable with package information with the following shape: @{ @@ -294,9 +293,8 @@ if ($AdditionalPackages) { Version = 123.4.56 Pin = $true } - #> +#> foreach ($package in $AdditionalPackages.GetEnumerator()) { - $PackageSplat = [System.Collections.Generic.list[string]]::new() $PackageSplat.add('install') $PackageSplat.add($package['Id']) diff --git a/scripts/Register-C4bEndpoint.ps1 b/scripts/Register-C4bEndpoint.ps1 index 627c280..650f63f 100644 --- a/scripts/Register-C4bEndpoint.ps1 +++ b/scripts/Register-C4bEndpoint.ps1 @@ -76,6 +76,7 @@ Param( [Parameter()] [String] $ProxyUrl, + # The credentials, if required, to connect to the proxy server. [Parameter()] [PSCredential] @@ -148,7 +149,7 @@ begin { } end { - # If we use a Self-Signed certificate, we need to explicity trust it + # If we use a Self-Signed certificate, we need to explicitly trust it if ($TrustCertificate) { Invoke-Expression ($downloader.DownloadString("http://$($Fqdn):80/Import-ChocoServerCertificate.ps1")) } From 1ae7763be0b42338fb05a54cbccf3f74b5fac29b Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Tue, 4 Mar 2025 18:17:15 +0000 Subject: [PATCH 09/17] (#291) Improves Idempotency of SQL Setup Previously, re-running could result in a user being in use and consequently not being dropped. --- modules/C4B-Environment/C4B-Environment.psm1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/C4B-Environment/C4B-Environment.psm1 b/modules/C4B-Environment/C4B-Environment.psm1 index 7a7948e..c8ce91f 100644 --- a/modules/C4B-Environment/C4B-Environment.psm1 +++ b/modules/C4B-Environment/C4B-Environment.psm1 @@ -540,6 +540,9 @@ function Add-DatabaseUserAndRoles { USE [master] IF EXISTS(SELECT * FROM msdb.sys.syslogins WHERE UPPER([name]) = UPPER('$Username')) BEGIN +DECLARE @kill varchar(8000) = ''; +SELECT @kill = @kill + 'kill ' + CONVERT(varchar(5), session_id) + ';' FROM sys.dm_exec_sessions WHERE UPPER([login_name]) = UPPER('$Username') +EXEC(@kill); DROP LOGIN [$Username] END From bf60025b77b3e38371868aa154046755a3076b9a Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Tue, 4 Mar 2025 18:41:02 +0000 Subject: [PATCH 10/17] (docs) Updates Script Names After discussion with Ryan, this results in a more obvious flow (and use) of script names. The quicklink and docs will need to be updated. --- Start-C4bSetup.ps1 => Initialize-C4bSetup.ps1 | 2 +- README.md | 8 ++++---- Set-SslSecurity.ps1 => Switch-SslSecurity.ps1 | 2 +- Start-C4bVerification.ps1 => Test-C4bSetup.ps1 | 0 tests/server.tests.ps1 | 13 ++++++------- 5 files changed, 12 insertions(+), 13 deletions(-) rename Start-C4bSetup.ps1 => Initialize-C4bSetup.ps1 (98%) rename Set-SslSecurity.ps1 => Switch-SslSecurity.ps1 (98%) rename Start-C4bVerification.ps1 => Test-C4bSetup.ps1 (100%) diff --git a/Start-C4bSetup.ps1 b/Initialize-C4bSetup.ps1 similarity index 98% rename from Start-C4bSetup.ps1 rename to Initialize-C4bSetup.ps1 index 030681e..b5a075b 100644 --- a/Start-C4bSetup.ps1 +++ b/Initialize-C4bSetup.ps1 @@ -121,7 +121,7 @@ $QsRepo = if ($Branch) { } $DefaultEap, $ErrorActionPreference = $ErrorActionPreference, 'Stop' -Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Start-C4bSetup-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" +Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Initialize-C4bSetup-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" try { # Setup initial choco-setup directories diff --git a/README.md b/README.md index 41eb4c1..35ab031 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Below are the minimum requirements for setting up your C4B server via this guide ```powershell Set-Location "$env:SystemDrive\choco-setup\files" -.\Start-C4bSetup.ps1 -Thumbprint '' +.\Initialize-C4bSetup.ps1 -Thumbprint '' ``` > :warning:**REMINDER**: If you are using your own SSL certificate, be sure to place this certificate in the `Local Machine > Personal` certificate store before running the above script, and ensure that the private key is exportable. @@ -124,14 +124,14 @@ Set-Location "$env:SystemDrive\choco-setup\files" ```powershell Set-Location "$env:SystemDrive\choco-setup\files" -.\Start-C4bSetup.ps1 -Thumbprint '' -CertificateDnsName '' +.\Initialize-C4bSetup.ps1 -Thumbprint '' -CertificateDnsName '' ``` For example, with a wildcard certificate with a thumbprint of `deee9b2fabb24bdaae71d82286e08de1` you wish to use `chocolatey.foo.org`, the following would be required: ```powershell Set-Location "$env:SystemDrive\choco-setup\files" -.\Start-C4bSetup.ps1 -Thumbprint deee9b2fabb24bdaae71d82286e08de1 -CertificateDnsName chocolatey.foo.org +.\Initialize-C4bSetup.ps1 -Thumbprint deee9b2fabb24bdaae71d82286e08de1 -CertificateDnsName chocolatey.foo.org ``` ### Step 2: Nexus Setup @@ -221,7 +221,7 @@ Set-Location "$env:SystemDrive\choco-setup\files" ```powershell Set-Location "$env:SystemDrive\choco-setup\files" - .\Start-C4bVerification.ps1 -Fqdn '' + .\Test-C4bSetup.ps1 -Fqdn '' ``` If you expect services to be available at `chocoserver.yourcompany.com`, then your command would look like: `.\Start-C4bVerification.ps1 -Fqdn 'chocoserver.yourcompany.com'` diff --git a/Set-SslSecurity.ps1 b/Switch-SslSecurity.ps1 similarity index 98% rename from Set-SslSecurity.ps1 rename to Switch-SslSecurity.ps1 index 42a638a..860474c 100644 --- a/Set-SslSecurity.ps1 +++ b/Switch-SslSecurity.ps1 @@ -67,7 +67,7 @@ param( process { $DefaultEap = $ErrorActionPreference $ErrorActionPreference = 'Stop' - Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Set-SslSecurity-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" + Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Switch-SslSecurity-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" # Collect current certificate configuration $Certificate = if ($Subject) { diff --git a/Start-C4bVerification.ps1 b/Test-C4bSetup.ps1 similarity index 100% rename from Start-C4bVerification.ps1 rename to Test-C4bSetup.ps1 diff --git a/tests/server.tests.ps1 b/tests/server.tests.ps1 index fbd2940..a695d7a 100644 --- a/tests/server.tests.ps1 +++ b/tests/server.tests.ps1 @@ -45,14 +45,13 @@ Describe "Server Integrity" { $Logs = Get-ChildItem C:\choco-setup\logs -Recurse -Filter *.txt } - It " log file was created during installation" -ForEach @( - @{File = 'Set-SslSecurity'} - @{File = 'Start-C4bCcmSetup'} - @{File = 'Start-C4bJenkinsSetup'} - @{File = 'Start-C4bNexusSetup'} - @{File = 'Start-C4bSetup'} + It "<_> log file was created during installation" -ForEach @( + 'Start-C4bCcmSetup' + 'Start-C4bJenkinsSetup' + 'Start-C4bNexusSetup' + 'Initialize-C4bSetup' ) { - Test-Path "C:\choco-setup\logs\$($_.File)*.txt" | Should -Be $true + Test-Path "C:\choco-setup\logs\$($_)*.txt" | Should -Be $true } } } From ac363f35d90b1826f63273cddda3d20e14bc9476 Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Tue, 4 Mar 2025 18:45:19 +0000 Subject: [PATCH 11/17] (#280) Updates CCM Website Root Address --- Start-C4bCcmSetup.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Start-C4bCcmSetup.ps1 b/Start-C4bCcmSetup.ps1 index c8cdb99..58f740c 100644 --- a/Start-C4bCcmSetup.ps1 +++ b/Start-C4bCcmSetup.ps1 @@ -237,6 +237,13 @@ process { Invoke-Choco config set centralManagementServiceCommunicationSaltAdditivePassword $ServiceSaltValue.ToPlainText() } + # Set Website Root Address + Update-CcmSettings -CcmEndpoint $CCmEndpoint -Credential $CCMCredential -Settings @{ + website = @{ + webSiteRootAddress = $CcmEndpoint + } + } + $CcmSvcUrl = Invoke-Choco config get centralManagementServiceUrl -r Update-Clixml -Properties @{ CCMServiceURL = $CcmSvcUrl From 49ce6331a91c24a4592e129de236c006766aba7e Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Mon, 17 Mar 2025 12:04:19 +0000 Subject: [PATCH 12/17] (#291) Adds Single-Entrypoint for Usage We should no longer tempt customer's fates by providing multiple things to enter into the terminal. Instead, we have a single thing to run, and we can drill in if we need to. --- Initialize-C4bSetup.ps1 | 158 ++++++++++++++++++-------------- README.md | 194 ++++++++++++++++++---------------------- Start-C4bCcmSetup.ps1 | 9 +- 3 files changed, 181 insertions(+), 180 deletions(-) diff --git a/Initialize-C4bSetup.ps1 b/Initialize-C4bSetup.ps1 index b5a075b..dbcdbd7 100644 --- a/Initialize-C4bSetup.ps1 +++ b/Initialize-C4bSetup.ps1 @@ -10,15 +10,14 @@ C4B Quick-Start Guide initial bootstrap script - Setup of local `choco-setup` directories - Download of Chocolatey packages required for setup #> -[CmdletBinding(DefaultParameterSetName="Attended")] +[CmdletBinding(DefaultParameterSetName = 'Prepare')] param( # Full path to Chocolatey license file. # Accepts any file, and moves and renames it correctly. # You can either define this as a parameter, or # script will prompt you for it. # Script will also validate expiry. - [Parameter(ParameterSetName='Unattended')] - [Parameter(ParameterSetName='Attended')] + [Parameter(ParameterSetName = 'Install')] [string] $LicenseFile = $( if (Test-Path $PSScriptRoot\files\chocolatey.license.xml) { @@ -41,40 +40,36 @@ param( } ), - # Unattended mode. Allows you to skip running the other scripts individually. - [Parameter(Mandatory, ParameterSetName='Unattended')] - [switch] - $Unattend, - # Specify a credential used for the ChocolateyManagement DB user. - # Only required in Unattend mode for the CCM setup script. + # Only required in install mode for the CCM setup script. # If not populated, the script will prompt for credentials. - [Parameter(ParameterSetName='Unattended')] + [Parameter(ParameterSetName = 'Install')] [System.Management.Automation.PSCredential] $DatabaseCredential = $( if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseUser) { (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseUser - } elseif ($PSCmdlet.ParameterSetName -eq 'Unattended') { - $Wshell = New-Object -ComObject Wscript.Shell - $null = $Wshell.Popup('You will now create a credential for the ChocolateyManagement DB user, to be used by CCM (document this somewhere).') - Get-Credential -UserName ChocoUser -Message 'Create a credential for the ChocolateyManagement DB user' + } elseif ($PSCmdlet.ParameterSetName -eq 'Install') { + [PSCredential]::new( + "chocodbuser", + (ConvertTo-SecureString "$(New-Guid)-$(New-Guid)" -Force -AsPlainText) + ) } ), # The certificate thumbprint that identifies the target SSL certificate in # the local machine certificate stores. - # Only used in Unattend mode for the SSL setup script. - [Parameter(ParameterSetName='Unattended')] + # Only used in install mode for the SSL setup script. + [Parameter(ParameterSetName = 'Install')] [ArgumentCompleter({ - Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { - [System.Management.Automation.CompletionResult]::new( - $_.Thumbprint, - $_.Thumbprint, - "ParameterValue", - ($_.Subject -replace "^CN=(?.+),?.*$",'${FQDN}') - ) - } - })] + Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { + [System.Management.Automation.CompletionResult]::new( + $_.Thumbprint, + $_.Thumbprint, + "ParameterValue", + ($_.Subject -replace "^CN=(?.+),?.*$", '${FQDN}') + ) + } + })] [string] $Thumbprint = $( if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { @@ -87,21 +82,27 @@ param( ), # If using a wildcard certificate, provide a DNS name you want to use to access services secured by the certificate.\ - [string]$CertificateDnsName = $( - if (-not (Get-Command Get-ChocoEnvironmentProperty -ErrorAction SilentlyContinue)) {. $PSScriptRoot\scripts\Get-Helpers.ps1} - Get-ChocoEnvironmentProperty CertSubject + [Parameter(ParameterSetName = 'Install')] + [Alias("FQDN")] + [string] + $CertificateDnsName = $( + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertSubject) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertSubject + } ), # If provided, shows all Chocolatey output. Otherwise, blissful quiet. - [switch]$ShowChocoOutput, + [switch] + $ShowChocoOutput, # The branch or Pull Request to download the C4B setup scripts from. # Defaults to main. - [string] [Alias('PR')] + [string] $Branch = $env:CHOCO_QSG_BRANCH, - # If provided, will skip launching the browser + # If provided, will skip launching the browser at the end of setup. + [Parameter(ParameterSetName = 'Install')] [switch]$SkipBrowserLaunch ) if ($ShowChocoOutput) { @@ -133,7 +134,7 @@ try { $TestDir = Join-Path $ChocoPath "tests" $xmlDir = Join-Path $ChocoPath "clixml" - @($ChocoPath, $FilesDir, $PkgsDir, $TempDir, $TestDir,$xmlDir) | ForEach-Object { + @($ChocoPath, $FilesDir, $PkgsDir, $TempDir, $TestDir, $xmlDir) | ForEach-Object { $null = New-Item -Path $_ -ItemType Directory -Force -ErrorAction Stop } @@ -151,57 +152,78 @@ try { # Add the Module Path and Import Helper Functions if (-not (Get-Module C4B-Environment -ListAvailable)) { if ($env:PSModulePath.Split(';') -notcontains "$FilesDir\modules") { - [Environment]::SetEnvironmentVariable("PSModulePath", "$env:PSModulePath;$FilesDir\modules" ,"Machine") + [Environment]::SetEnvironmentVariable("PSModulePath", "$env:PSModulePath;$FilesDir\modules" , "Machine") $env:PSModulePath = [Environment]::GetEnvironmentVariables("Machine").PSModulePath } } Import-Module C4B-Environment -Verbose:$false - Update-Clixml -Properties @{ - InitialDeployment = Get-Date - } - - if ($Thumbprint) { - Set-ChocoEnvironmentProperty CertThumbprint $Thumbprint - - # Collect current certificate configuration - $Certificate = Get-Certificate -Thumbprint $Thumbprint - Copy-CertToStore -Certificate $Certificate - - $null = Test-CertificateDomain -Thumbprint $Thumbprint - } elseif ($PSScriptRoot) { - # We're going to be using a self-signed certificate - Set-ChocoEnvironmentProperty CertSubject $env:ComputerName - } - - if ($DatabaseCredential) { - Set-ChocoEnvironmentProperty DatabaseUser $DatabaseCredential - } - # Downloading all CCM setup packages below Write-Host "Downloading missing nupkg files to $($PkgsDir)." -ForegroundColor Green Write-Host "This will take some time. Feel free to get a tea or coffee." -ForegroundColor Green & $FilesDir\OfflineInstallPreparation.ps1 -LicensePath $LicenseFile - if (Test-Path $FilesDir\files\*.nupkg) { - Invoke-Choco source add --name LocalChocolateySetup --source $FilesDir\files\ --Priority 1 - } + # Kick off unattended running of remaining setup scripts, if we're running from a saved-script. + if ($PSScriptRoot -or $PSCmdlet.ParameterSetName -eq 'Install') { + Update-Clixml -Properties @{ + InitialDeployment = Get-Date + } + + if ($Thumbprint) { + Set-ChocoEnvironmentProperty CertThumbprint $Thumbprint + + if ($CertificateDnsName) { + Set-ChocoEnvironmentProperty CertSubject $CertificateDnsName + } + + # Collect current certificate configuration + $Certificate = Get-Certificate -Thumbprint $Thumbprint + Copy-CertToStore -Certificate $Certificate + + $null = Test-CertificateDomain -Thumbprint $Thumbprint + } elseif ($PSScriptRoot) { + # We're going to be using a self-signed certificate + if (-not $CertificateDnsName) { + $CertificateDnsName = $env:ComputerName + } + + $CertificateArgs = @{ + CertStoreLocation = "Cert:\LocalMachine\My" + KeyUsage = "KeyEncipherment", "DigitalSignature" + DnsName = $CertificateDnsName + NotAfter = (Get-Date).AddYears(10) + } + + $Certificate = New-SelfSignedCertificate @CertificateArgs + Copy-CertToStore -Certificate $Certificate + + $Thumbprint = $Certificate.Thumbprint + + Set-ChocoEnvironmentProperty CertThumbprint $Thumbprint + Set-ChocoEnvironmentProperty CertSubject $CertificateDnsName + } - # Set Choco Server Chocolatey Configuration - Invoke-Choco feature enable --name="'excludeChocolateyPackagesDuringUpgradeAll'" - Invoke-Choco feature enable --name="'usePackageHashValidation'" + if ($DatabaseCredential) { + Set-ChocoEnvironmentProperty DatabaseUser $DatabaseCredential + } + + if (Test-Path $FilesDir\files\*.nupkg) { + Invoke-Choco source add --name LocalChocolateySetup --source $FilesDir\files\ --Priority 1 + } - # Convert license to a "choco-license" package, and install it locally to test - Write-Host "Creating a 'chocolatey-license' package, and testing install." -ForegroundColor Green - Set-Location $FilesDir - .\scripts\Create-ChocoLicensePkg.ps1 - Remove-Item "$env:SystemDrive\choco-setup\packaging" -Recurse -Force + # Set Choco Server Chocolatey Configuration + Invoke-Choco feature enable --name="'excludeChocolateyPackagesDuringUpgradeAll'" + Invoke-Choco feature enable --name="'usePackageHashValidation'" + + # Convert license to a "choco-license" package, and install it locally to test + Write-Host "Creating a 'chocolatey-license' package, and testing install." -ForegroundColor Green + Set-Location $FilesDir + .\scripts\Create-ChocoLicensePkg.ps1 + Remove-Item "$env:SystemDrive\choco-setup\packaging" -Recurse -Force - # Kick off unattended running of remaining setup scripts. - if ($Unattend) { $Certificate = @{} - if ($Thumbprint) {$Certificate.Thumbprint = $Thumbprint} + if ($Thumbprint) { $Certificate.Thumbprint = $Thumbprint } Set-Location "$env:SystemDrive\choco-setup\files" .\Start-C4BNexusSetup.ps1 @Certificate diff --git a/README.md b/README.md index 35ab031..eeb1582 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Below is the Quick Start Guide as it exists currently on the [Chocolatey Docs](h --- -Welcome to the Chocolatey of Business (C4B) Quick-Start Guide! This guide will walk you through the basics of configuring a C4B Server on your VM infrastructure of choice. This includes: +Welcome to the Chocolatey for Business (C4B) Quick-Start Guide! This guide will walk you through the basics of configuring a C4B Server on your VM infrastructure of choice. This includes: - The Chocolatey Licensed components - A NuGet V3 Repository (Nexus) @@ -90,6 +90,8 @@ Below are the minimum requirements for setting up your C4B server via this guide Set-ExecutionPolicy Bypass -Scope Process -Force [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::tls12 Invoke-RestMethod https://ch0.co/qsg-go | Invoke-Expression + Set-Location "$env:SystemDrive\choco-setup\files" + .\Initialize-C4bSetup.ps1 ``` >
@@ -134,114 +136,88 @@ Set-Location "$env:SystemDrive\choco-setup\files" .\Initialize-C4bSetup.ps1 -Thumbprint deee9b2fabb24bdaae71d82286e08de1 -CertificateDnsName chocolatey.foo.org ``` -### Step 2: Nexus Setup - -1. In the same **elevated** Windows PowerShell console as above, paste and run the following code: - - ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Start-C4bNexusSetup.ps1 - ``` - - >
- > What does this script do? (click to expand) - >
    - >
  • Installs Sonatype Nexus Repository Manager OSS instance
  • - >
  • Cleans up all demo repositories on Nexus
  • - >
  • Creates a "ChocolateyInternal" NuGet repository
  • - >
  • Creates a "ChocolateyTest" NuGet repository
  • - >
  • Creates a "choco-install" raw repository
  • - >
  • Sets up "ChocolateyInternal" on C4B Server as source, with API key
  • - >
  • Adds firewall rule for repository access
  • - >
  • Installs MS Edge, as Internet Explorer cannot access the Sonatype Nexus site
  • - >
  • Outputs data to a JSON file to pass between scripts
  • - >
- >
- -### Step 3: Chocolatey Central Management Setup - -1. In the same PowerShell Administrator console as above, paste and run the following code: - - ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Start-C4bCcmSetup.ps1 - ``` - - >
- > What does this script do? (click to expand) - >
    - >
  • Installs MS SQL Express and SQL Server Management Studio (SSMS)
  • - >
  • Creates "ChocolateyManagement" database, and adds appropriate `ChocoUser` permissions
  • - >
  • Installs all 3 Chocolatey Central Management packages (database, service, web), with correct parameters
  • - >
  • Outputs data to a JSON file to pass between scripts
  • - >
- >
- -### Step 4: Jenkins Setup - -1. In the same **elevated** PowerShell console as above, paste and run the following code: - - ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Start-C4bJenkinsSetup.ps1 - ``` - - >
- > What does this script do? (click to expand) - >
    - >
  • Installs Jenkins package
  • - >
  • Updates Jenkins plugins
  • - >
  • Configures pre-downloaded Jenkins scripts for Package Internalizer automation
  • - >
  • Sets up pre-defined Jenkins jobs for the scripts above
  • - >
- >
- -### Step 5: Complete Setup - -1. In the same **elevated** PowerShell console as above, paste and run the following code: - - ```powershell - Complete-C4bSetup - ``` - - >
- > What does this script do? (click to expand) - >
    - >
  • Sets up Chocolatey Agent on this system
  • - >
  • Writes a Readme.html file to the Public Desktop with account information for C4B services
  • - >
  • Auto-opens README, CCM, Nexus, and Jenkins in your web browser
  • - >
- >
- - > :mag: **FYI**: A `Readme.html` file will now be generated on your desktop. This file contains login information for all 3 web portals (CCM, Nexus, and Jenkins). This `Readme.html`, along with all 3 web portals, will automatically be opened in your browser. - -### Step 6: Verification - -1. In the same **elevated** PowerShell console as above, paste and run the following code: - - ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Test-C4bSetup.ps1 -Fqdn '' - ``` - - If you expect services to be available at `chocoserver.yourcompany.com`, then your command would look like: `.\Start-C4bVerification.ps1 -Fqdn 'chocoserver.yourcompany.com'` - - >
- > What does this script do? (click to expand) - >
    - >
  • Verifies Nexus Repository installation
  • - >
  • Verifies Central Management installation
  • - >
  • Verifies Jenkins installation
  • - >
  • Ensures system firewall is configured
  • - >
  • Ensures Windows Features are installed
  • - >
  • Ensures services are correctly configured
  • - >
  • Ensured README is created
  • - >
- >
- -### Step 7: Setting up Endpoints - -1. Find the `Register-C4bEndpoint.ps1` script in the `choco-setup\files\scripts\` directory on your C4B Server. Copy this script to your client endpoint. +#### Script: Nexus Setup + +As part of the C4B setup, we install and configure Sonatype Nexus Repository, which is used for hosting your Chocolatey packages internally: + +>
+> What does this script do? (click to expand) +>
    +>
  • Installs Sonatype Nexus Repository Manager OSS instance
  • +>
  • Cleans up all demo repositories on Nexus
  • +>
  • Creates a "ChocolateyInternal" NuGet repository
  • +>
  • Creates a "ChocolateyTest" NuGet repository
  • +>
  • Creates a "choco-install" raw repository
  • +>
  • Sets up "ChocolateyInternal" on C4B Server as source, with API key
  • +>
  • Adds firewall rule for repository access
  • +>
  • Installs MS Edge, as Internet Explorer cannot access the Sonatype Nexus site
  • +>
  • Outputs data to a JSON file to pass between scripts
  • +>
+>
+ +#### Script: Chocolatey Central Management Setup + +As part of the C4B setup, we install and configure Chocolatey Central Management and associated prerequisites to manage your Chocolatey for Business nodes: + +>
+> What does this script do? (click to expand) +>
    +>
  • Installs MS SQL Express and SQL Server Management Studio (SSMS)
  • +>
  • Creates "ChocolateyManagement" database, and adds appropriate `ChocoUser` permissions
  • +>
  • Installs all 3 Chocolatey Central Management packages (database, service, web), with correct parameters
  • +>
  • Outputs data to a JSON file to pass between scripts
  • +>
+>
+ +#### Script: Jenkins Setup + +As part of the C4B setup, we install and configure Jenkins as a method for running Package Internalizer and other jobs: + +>
+> What does this script do? (click to expand) +>
    +>
  • Installs Jenkins package
  • +>
  • Updates Jenkins plugins
  • +>
  • Configures pre-downloaded Jenkins scripts for Package Internalizer automation
  • +>
  • Sets up pre-defined Jenkins jobs for the scripts above
  • +>
+>
+ +#### Script: Complete Setup + +As part of the C4B setup, we create a readme and install the Chocolatey Agent on the server: + +>
+> What does this script do? (click to expand) +>
    +>
  • Sets up Chocolatey Agent on this system
  • +>
  • Writes a Readme.html file to the Public Desktop with account information for C4B services
  • +>
  • Auto-opens README, CCM, Nexus, and Jenkins in your web browser
  • +>
+>
+ +> :mag: **FYI**: A `Readme.html` file will now be generated on your desktop. This file contains login information for all 3 web portals (CCM, Nexus, and Jenkins). This `Readme.html`, along with all 3 web portals, will automatically be opened in your browser. + +### Script: Verification + +As a part of the C4B setup, we run tests to validate that your environment is correctly configured: + +>
+> What does this script do? (click to expand) +>
    +>
  • Verifies Nexus Repository installation
  • +>
  • Verifies Central Management installation
  • +>
  • Verifies Jenkins installation
  • +>
  • Ensures system firewall is configured
  • +>
  • Ensures Windows Features are installed
  • +>
  • Ensures services are correctly configured
  • +>
  • Ensured README is created
  • +>
+>
+ +### Step 2: Setting up Endpoints + +1. Find the `Register-C4bEndpoint.ps1` script in the `C:\choco-setup\files\scripts\` directory on your C4B Server. Copy this script to your client endpoint. 1. Open an **elevated** PowerShell console on your client endpoint, and browse (`cd`) to the location you copied the script above. Paste and run the following code: diff --git a/Start-C4bCcmSetup.ps1 b/Start-C4bCcmSetup.ps1 index 58f740c..77e0eb8 100644 --- a/Start-C4bCcmSetup.ps1 +++ b/Start-C4bCcmSetup.ps1 @@ -16,10 +16,13 @@ param( [ValidateNotNull()] [System.Management.Automation.PSCredential] $DatabaseCredential = $( - if ($DatabaseCredential = Get-ChocoEnvironmentProperty DatabaseUser) { - $DatabaseCredential + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseUser) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseUser } else { - Get-Credential -Username ChocoUser -Message 'Create a credential for the ChocolateyManagement DB user (document this somewhere)' + [PSCredential]::new( + "chocodbuser", + (ConvertTo-SecureString "$(New-Guid)-$(New-Guid)" -Force -AsPlainText) + ) } ), From d91d227fe0d79835ac43c6a284b7395900e8e9d1 Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Fri, 2 May 2025 17:22:08 +0100 Subject: [PATCH 13/17] (#291) Allow for Beta Testing CCM Packages The installer should allow for testing beta packages where available, as long as we've specified the version. This should control that more precisely. --- Start-C4bCcmSetup.ps1 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Start-C4bCcmSetup.ps1 b/Start-C4bCcmSetup.ps1 index 77e0eb8..95f3a74 100644 --- a/Start-C4bCcmSetup.ps1 +++ b/Start-C4bCcmSetup.ps1 @@ -135,6 +135,9 @@ process { Write-Host "Creating Chocolatey Central Management Database" $chocoArgs = @('install', 'chocolatey-management-database', '--source="ChocolateyInternal"', '-y', '--package-parameters="''/ConnectionString=Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;Trusted_Connection=true;''"', '--no-progress') + if ($PackageVersion = $Packages.Where{ $_.Name -eq 'chocolatey-management-database' }.Version) { + $chocoArgs += "--version='$($PackageVersion)'" + } & Invoke-Choco @chocoArgs # Add Local Windows User: @@ -153,6 +156,9 @@ process { Write-Host "Installing Chocolatey Central Management Service" $chocoArgs = @('install', 'chocolatey-management-service', "--source='ChocolateyInternal'", '-y', "--package-parameters-sensitive=`"/ConnectionString:'Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;User ID=$DatabaseUser;Password=$DatabaseUserPw;'`"", '--no-progress') + if ($PackageVersion = $Packages.Where{ $_.Name -eq 'chocolatey-management-service' }.Version) { + $chocoArgs += "--version='$($PackageVersion)'" + } if ($Thumbprint) { Write-Verbose "Validating certificate is in LocalMachine\TrustedPeople Store" if (-not (Get-Item Cert:\LocalMachine\TrustedPeople\$Thumbprint -EA 0) -and -not (Get-Item Cert:\LocalMachine\My\$Thumbprint -EA 0)) { @@ -169,10 +175,13 @@ process { } & Invoke-Choco @chocoArgs - if (-not $MyCertificate) {$MyCertificate = Get-Item Cert:\LocalMachine\My\*} + if (-not $MyCertificate) { $MyCertificate = Get-Item Cert:\LocalMachine\My\* } Write-Host "Installing Chocolatey Central Management Website" $chocoArgs = @('install', 'chocolatey-management-web', "--source='ChocolateyInternal'", '-y', "--package-parameters-sensitive=""'/ConnectionString:Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;User ID=$DatabaseUser;Password=$DatabaseUserPw;'""", '--no-progress') + if ($PackageVersion = $Packages.Where{ $_.Name -eq 'chocolatey-management-web' }.Version) { + $chocoArgs += "--version='$($PackageVersion)'" + } & Invoke-Choco @chocoArgs # Setup Website SSL From d579e83ae569bae2100268e6f7d15ae3407ef69e Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Fri, 2 May 2025 17:46:36 +0100 Subject: [PATCH 14/17] (#291) Update Jenkins Plugin Versions --- files/jenkins.json | 58 +++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/files/jenkins.json b/files/jenkins.json index da5e171..ba32c66 100644 --- a/files/jenkins.json +++ b/files/jenkins.json @@ -1,33 +1,33 @@ { "plugins": [ - { "name": "apache-httpcomponents-client-4-api", "version": "4.5.14-208.v438351942757" }, - { "name": "asm-api", "version": "9.7-33.v4d23ef79fcc8" }, - { "name": "bouncycastle-api", "version": "2.30.1.78.1-233.vfdcdeb_0a_08a_a_" }, - { "name": "branch-api", "version": "2.1169.va_f810c56e895" }, - { "name": "caffeine-api", "version": "3.1.8-133.v17b_1ff2e0599" }, - { "name": "cloudbees-folder", "version": "6.928.v7c780211d66e" }, - { "name": "display-url-api", "version": "2.204.vf6fddd8a_8b_e9" }, - { "name": "durable-task", "version": "555.v6802fe0f0b_82" }, - { "name": "instance-identity", "version": "185.v303dc7c645f9" }, - { "name": "ionicons-api", "version": "74.v93d5eb_813d5f" }, - { "name": "jakarta-activation-api", "version": "2.1.3-1" }, - { "name": "jakarta-mail-api", "version": "2.1.3-1" }, - { "name": "javax-activation-api", "version": "1.2.0-7" }, - { "name": "javax-mail-api", "version": "1.6.2-10" }, - { "name": "mailer", "version": "472.vf7c289a_4b_420" }, - { "name": "pipeline-groovy-lib", "version": "710.v4b_94b_077a_808" }, - { "name": "scm-api", "version": "690.vfc8b_54395023" }, - { "name": "script-security", "version": "1341.va_2819b_414686" }, - { "name": "structs", "version": "337.v1b_04ea_4df7c8" }, - { "name": "variant", "version": "60.v7290fc0eb_b_cd" }, - { "name": "workflow-api", "version": "1316.v33eb_726c50b_a_" }, - { "name": "workflow-basic-steps", "version": "1058.vcb_fc1e3a_21a_9" }, - { "name": "workflow-cps", "version": "3894.3896.vca_2c931e7935" }, - { "name": "workflow-durable-task-step", "version": "1353.v1891a_b_01da_18" }, - { "name": "workflow-job", "version": "1400.v7fd111b_ec82f" }, - { "name": "workflow-multibranch", "version": "783.va_6eb_ef636fb_d" }, - { "name": "workflow-scm-step", "version": "427.v4ca_6512e7df1" }, - { "name": "workflow-step-api", "version": "657.v03b_e8115821b_" }, - { "name": "workflow-support", "version": "907.v6713a_ed8a_573" } + { "name": "apache-httpcomponents-client-4-api", "version": "4.5.14-269.vfa_2321039a_83" }, + { "name": "asm-api", "version": "9.8-135.vb_2239d08ee90" }, + { "name": "bouncycastle-api", "version": "2.30.1.80-256.vf98926042a_9b_" }, + { "name": "branch-api", "version": "2.1217.v43d8b_b_d8b_2c7" }, + { "name": "caffeine-api", "version": "3.2.0-166.v72a_6d74b_870f" }, + { "name": "cloudbees-folder", "version": "6.1012.v79a_86a_1ea_c1f" }, + { "name": "display-url-api", "version": "2.209.v582ed814ff2f" }, + { "name": "durable-task", "version": "587.v84b_877235b_45" }, + { "name": "instance-identity", "version": "201.vd2a_b_5a_468a_a_6" }, + { "name": "ionicons-api", "version": "82.v0597178874e1" }, + { "name": "jakarta-activation-api", "version": "2.1.3-2" }, + { "name": "jakarta-mail-api", "version": "2.1.3-2" }, + { "name": "javax-activation-api", "version": "1.2.0-8" }, + { "name": "javax-mail-api", "version": "1.6.2-11" }, + { "name": "mailer", "version": "489.vd4b_25144138f" }, + { "name": "pipeline-groovy-lib", "version": "752.vdddedf804e72" }, + { "name": "scm-api", "version": "704.v3ce5c542825a_" }, + { "name": "script-security", "version": "1373.vb_b_4a_a_c26fa_00" }, + { "name": "structs", "version": "343.vdcf37b_a_c81d5" }, + { "name": "variant", "version": "70.va_d9f17f859e0" }, + { "name": "workflow-api", "version": "1371.ve334280b_d611" }, + { "name": "workflow-basic-steps", "version": "1079.vce64b_a_929c5a_" }, + { "name": "workflow-cps", "version": "4046.v90b_1b_9edec67" }, + { "name": "workflow-durable-task-step", "version": "1405.v1fcd4a_d00096" }, + { "name": "workflow-job", "version": "1520.v56d65e3b_4566" }, + { "name": "workflow-multibranch", "version": "806.vb_b_688f609ee9" }, + { "name": "workflow-scm-step", "version": "437.v05a_f66b_e5ef8" }, + { "name": "workflow-step-api", "version": "700.v6e45cb_a_5a_a_21" }, + { "name": "workflow-support", "version": "968.v8f17397e87b_8" } ] } \ No newline at end of file From d8d5de349d6b4eb8945c5555aca4afb0f4618147 Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Tue, 6 May 2025 10:30:06 +0100 Subject: [PATCH 15/17] (#291) Improve Edge Firstrun Experience ...by successfully removing it. --- Start-C4bNexusSetup.ps1 | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/Start-C4bNexusSetup.ps1 b/Start-C4bNexusSetup.ps1 index d1676b1..910a23f 100644 --- a/Start-C4bNexusSetup.ps1 +++ b/Start-C4bNexusSetup.ps1 @@ -267,19 +267,12 @@ process { if (-not (Test-Path 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe')) { Write-Host "Installing Microsoft Edge, to allow viewing the Nexus site" Invoke-Choco install microsoft-edge -y --source ChocolateyInternal - if ($LASTEXITCODE -eq 0) { - if (Test-Path 'HKLM:\SOFTWARE\Microsoft\Edge') { - $RegArgs = @{ - Path = 'HKLM:\SOFTWARE\Microsoft\Edge\' - Name = 'HideFirstRunExperience' - Type = 'Dword' - Value = 1 - Force = $true - } - $null = Set-ItemProperty @RegArgs - } - } } + $Key = @{ Key = 'HKLM:\Software\Policies\Microsoft\Edge' ; Value = 'HideFirstRunExperience' } + if (-not (Test-Path $Key.Key)) { + $null = New-Item -Path $Key.Key -Force + } + $null = New-ItemProperty -Path $Key.Key -Name $Key.Value -Value 1 -PropertyType DWORD -Force # Save useful params Update-Clixml -Properties @{ From 9b2def1e1762c26c65dac25ef806788ab9ff821b Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Thu, 22 May 2025 12:23:41 +0100 Subject: [PATCH 16/17] (maint) Update Packages This updates Chocolatey dependencies within the repository to the latest available versions. --- files/chocolatey.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/files/chocolatey.json b/files/chocolatey.json index 4a5eb06..34e1130 100644 --- a/files/chocolatey.json +++ b/files/chocolatey.json @@ -14,9 +14,9 @@ { "name": "chocolatey" }, { "name": "chocolateygui.extension" }, { "name": "chocolateygui" }, - { "name": "dotnet-8.0-aspnetruntime", "version": "8.0.8" }, - { "name": "dotnet-8.0-runtime", "version": "8.0.8" }, - { "name": "dotnet-aspnetcoremodule-v2", "version": "18.0.24201" }, + { "name": "dotnet-8.0-aspnetruntime", "version": "8.0.16" }, + { "name": "dotnet-8.0-runtime", "version": "8.0.16" }, + { "name": "dotnet-aspnetcoremodule-v2", "version": "18.0.25074" }, { "name": "dotnetfx" }, { "name": "jenkins" }, { "name": "KB2919355", "internalize": false }, From df2af33d2177e50528927df3e4873d0935db8958 Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Thu, 22 May 2025 13:10:23 +0100 Subject: [PATCH 17/17] (docs) Adds Endpoint Configuration Doc to Readme I found this was missing when I trued up the docs PR. --- README.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eeb1582..8bef2c3 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ Set-Location "$env:SystemDrive\choco-setup\files" .\Initialize-C4bSetup.ps1 -Thumbprint '' ``` -> :warning:**REMINDER**: If you are using your own SSL certificate, be sure to place this certificate in the `Local Machine > Personal` certificate store before running the above script, and ensure that the private key is exportable. +> :warning:**REMINDER**: If you are using your own SSL certificate, be sure to place this certificate in the `Local Machine > Trusted People` certificate store before running the above script, and ensure that the private key is exportable. > :memo: **NOTE** > A Role and User credential will be configured to limit access to your Nexus repositories. As well, CCM Client and Service Salts are configured to further encrypt your connection between CCM and your endpoint clients. These additional settings are also incorporated into your `Register-C4bEndpoint.ps1` script for onboarding endpoints. @@ -241,6 +241,35 @@ As a part of the C4B setup, we run tests to validate that your environment is co > >
+#### Available parameters + +* `ClientCommunicationSalt` + The salt for communication from an agent to an instance of Central Management Service. Details available in the README file on server desktop. +* `ServiceCommunicationSalt` + The salt for communication from an instance of Central Management Service to an agent. Details available in the README file on server desktop. +* `RepositoryCredential` + The credential to use to access the repository server from the endpoint. Details available in the README file on server desktop. +* `ProxyUrl` + The URL of a proxy server to use for connecting to the repository. +* `ProxyCredential` + The credentials, if required, to connect to the proxy server. +* `IncludePackageTools` + Install the Chocolatey Licensed Extension with right-click context menus available. +* `AdditionalConfiguration` + Allows for the application of user-defined configuration that is applied after the base configuration. +* `AdditionalFeatures` + Allows for the toggling of additional features that is applied after the base configuration. +* `AdditionalPackages` + Allows for the installation of additional packages after the system base packages have been installed. +* `AdditionalSources` + Allows for the addition of alternative sources after the base configuration has been applied. +* `TrustCertificate` + If passed, downloads the certificate from the client server before initializing Chocolatey Agent. + +#### Advanced Endpoint Configuration + +It is possible to customize the installation of Chocolatey on an endpoint via the available parameters above. For examples, please see [Advanced Endpoint Configuration](https://docs.chocolatey.org/en-us/c4b-environments/quick-start-environment/advanced-client-configuration/). + ### Conclusion Congratulations! If you followed all the steps detailed above, you should now have a fully functioning Chocolatey for Business implementation deployed in your environment.