├── .github └── workflows │ ├── Helpers.psm1 │ └── test.yml ├── .gitignore ├── CONTRIBUTING.md ├── Initialize-C4bSetup.ps1 ├── LICENSE ├── OfflineInstallPreparation.ps1 ├── README.md ├── Start-C4bCcmSetup.ps1 ├── Start-C4bJenkinsSetup.ps1 ├── Start-C4bNexusSetup.ps1 ├── Switch-SslSecurity.ps1 ├── Test-C4bSetup.ps1 ├── c4b-server.png ├── files ├── chocolatey.json └── jenkins.json ├── modules └── C4B-Environment │ ├── C4B-Environment.psd1 │ ├── C4B-Environment.psm1 │ └── ReadmeTemplate.html.j2 ├── scripts ├── ClientSetup.ps1 ├── Create-ChocoLicensePkg.ps1 ├── Get-Helpers.ps1 ├── Import-ChocoServerCertificate.ps1 ├── New-IISCertificateHost.ps1 ├── Register-C4bEndpoint.ps1 ├── Set-CCMCert.ps1 ├── Set-JenkinsCert.ps1 └── Set-NexusCert.ps1 └── tests ├── ccm.tests.ps1 ├── jenkins.tests.ps1 ├── nexus.tests.ps1 ├── packages.ps1 └── server.tests.ps1 /.github/workflows/Helpers.psm1: -------------------------------------------------------------------------------- 1 | function New-TestVM { 2 | [CmdletBinding()] 3 | param( 4 | [Parameter(Mandatory)] 5 | [string]$ResourceGroupName, 6 | 7 | [Parameter()] 8 | [string]$Name = "qsg-$((New-Guid).ToString() -replace '-' -replace '^(.{11}).+$', '$1')", 9 | 10 | [ValidateSet("Win2022AzureEditionCore", "Win2019Datacenter")] 11 | [string]$Image = "Win2022AzureEditionCore", 12 | 13 | [ArgumentCompleter({ 14 | param($a,$b,$WordToComplete,$d,$e) 15 | if (-not $script:VmSizes) { 16 | $script:VmSizes = Get-AzVMSize -Location 'eastus2' 17 | } 18 | $script:VmSizes.Name.Where{$_ -like "*$WordToComplete*"} 19 | })] 20 | [string]$Size = 'Standard_B4ms' 21 | ) 22 | 23 | if (-not (Get-AzVM -ResourceGroupName $ResourceGroupName -Name $Name -ErrorAction SilentlyContinue)) { 24 | $VmArgs = @{ 25 | ResourceGroup = $ResourceGroupName 26 | Name = $Name 27 | PublicIpAddressName = "$Name-ip" 28 | DomainNameLabel = $Name 29 | PublicIpSku = 'Basic' 30 | Image = $Image 31 | Size = $Size 32 | SecurityGroupName = "$ResourceGroupName-nsg" 33 | VirtualNetworkName = "$Name-vnet" 34 | NetworkInterfaceDeleteOption = 'Delete' 35 | OSDiskDeleteOption = 'Delete' 36 | Credential = [PSCredential]::new( 37 | 'ccmadmin', 38 | (ConvertTo-SecureString "$(New-Guid)" -AsPlainText -Force) 39 | ) 40 | } 41 | 42 | Write-Host "Creating VM '$($VmArgs.Name)'" 43 | $VM = New-AzVM @VmArgs 44 | $VM | Add-Member -MemberType NoteProperty -Name Credential -Value $VmArgs.Credential -PassThru 45 | } 46 | } 47 | 48 | function Request-WinRmAccessForTesting { 49 | param( 50 | [Parameter(Mandatory)] 51 | [string]$ResourceGroupName, 52 | 53 | [Parameter()] 54 | [string]$VmName, 55 | 56 | [Parameter()] 57 | [string]$IpAddress = $(Invoke-RestMethod https://api.ipify.org) 58 | ) 59 | while (!$Success) { 60 | if ($NetworkSecurityGroup = Get-AzNetworkSecurityGroup -ResourceGroupName $ResourceGroupName -Name $ResourceGroupName-nsg -ErrorAction SilentlyContinue) { 61 | $RuleArgs = @{ 62 | NetworkSecurityGroup = $NetworkSecurityGroup 63 | Name = "AllowWinRMSecure$($IpAddress -replace '\.')" 64 | Description = "Allow WinRM over HTTPS for '$($IpAddress)'" 65 | Access = "Allow" 66 | Protocol = "Tcp" 67 | Direction = "Inbound" 68 | Priority = 300 69 | SourceAddressPrefix = $IpAddress 70 | SourcePortRange = "*" 71 | DestinationAddressPrefix = "*" 72 | DestinationPortRange = 5986 73 | } 74 | 75 | if (($Rules = Get-AzNetworkSecurityRuleConfig -NetworkSecurityGroup $NetworkSecurityGroup).Name -notcontains $RuleArgs.Name) { 76 | Write-Host "Adding WinRM Rule to '$($NetworkSecurityGroup.Name)'" 77 | while ($Rules.Priority -contains $RuleArgs.Priority) { 78 | $RuleArgs.Priority++ 79 | } 80 | $NewRules = Add-AzNetworkSecurityRuleConfig @RuleArgs 81 | } 82 | } 83 | 84 | try { 85 | $Success = if ($NewRules) { 86 | Set-AzNetworkSecurityGroup -NetworkSecurityGroup $NetworkSecurityGroup -ErrorAction Stop 87 | } else { 88 | $true # Nothing to do 89 | } 90 | } catch { 91 | if ("$_" -match 'CanceledAndSupersededDueToAnotherOperation') { 92 | Write-Verbose "Another operation was occuring on '$($ResourceGroupName)-nsg' - retrying in 30 seconds." 93 | Start-Sleep -Seconds 30 94 | } else { 95 | throw 96 | } 97 | } 98 | } 99 | 100 | if ($VmName) { 101 | Write-Host "Enabling Remote PowerShell on '$($VMName)'" 102 | $null = Invoke-AzVMRunCommand -ResourceGroupName $ResourceGroupName -Name $VMName -CommandId EnableRemotePS 103 | } 104 | } 105 | 106 | function New-HoplessRemotingSession { 107 | [CmdletBinding()] 108 | [OutputType([System.Management.Automation.Runspaces.PSSession])] 109 | param( 110 | # The address to connect to 111 | [Parameter(Mandatory)] 112 | [string]$ComputerName, 113 | 114 | # The credential for the session 115 | [Parameter(Mandatory)] 116 | [PSCredential]$Credential 117 | ) 118 | Write-Host "Creating remoting session for $($Credential.UserName)@$($ComputerName)" 119 | $RemotingArgs = @{ 120 | ComputerName = $ComputerName 121 | Credential = $Credential 122 | UseSSL = $true 123 | SessionOption = New-PSSessionOption -SkipCACheck -SkipCNCheck 124 | } 125 | $Timeout = [System.Diagnostics.Stopwatch]::StartNew() 126 | while (-not $Session -and $Timeout.Elapsed.TotalSeconds -lt 300) { 127 | try { 128 | $Session = New-PSSession @RemotingArgs -ConfigurationName Hopless -ErrorAction Stop 129 | } catch [System.Management.Automation.Remoting.PSRemotingTransportException] { 130 | if ($_.Exception.Message -match 'Cannot find the Hopless session configuration') { 131 | Write-Host "Creating Hopless Configuration for $($Credential.UserName)@$($ComputerName)" 132 | # This throws an error when the session terminates, so we catch the PSRemotingTransportException 133 | try { 134 | $null = Invoke-Command @RemotingArgs { 135 | Register-PSSessionConfiguration -Name "Hopless" -RunAsCredential $using:Credential -Force -WarningAction SilentlyContinue 136 | } -ErrorAction Stop 137 | } catch [System.Management.Automation.Remoting.PSRemotingTransportException] {} 138 | 139 | Write-Verbose "Recreating Session after WinRM restart..." 140 | $Timeout = [System.Diagnostics.Stopwatch]::StartNew() 141 | while ($Session.Availability -ne 'Available' -and $Timeout.Elapsed.TotalSeconds -lt 180) { 142 | try { 143 | $Session = New-PSSession @RemotingArgs -ConfigurationName Hopless -ErrorAction Stop 144 | } catch { 145 | Start-Sleep -Seconds 10 146 | } 147 | } 148 | 149 | if ($Session.Availability -ne 'Available') { 150 | $Session 151 | Write-Error "Failed to re-establish a connection to '$($ComputerName)'" 152 | } else { 153 | Write-Host "Successfully reconnected after '$($Timeout.Elapsed.TotalSeconds)' seconds" 154 | } 155 | } elseif ($_.Exception.Message -match 'WinRM cannot complete the operation') { 156 | Write-Verbose "Retrying connection in 30 seconds..." 157 | Start-Sleep -Seconds 30 158 | if ($Timeout.Elapsed.TotalSeconds -gt 300) { 159 | Write-Error "Failed to connect after $($Timeout.Elapsed.TotalMinutes) minutes" 160 | throw 161 | } 162 | } else {throw} 163 | } 164 | } 165 | return $Session 166 | } 167 | 168 | function Install-DotNet { 169 | [OutputType([int32])] 170 | [CmdletBinding()] 171 | param( 172 | [Parameter(Mandatory, ParameterSetName = 'Separate')] 173 | [string]$ComputerName, 174 | 175 | [Parameter(Mandatory, ParameterSetName = 'Separate')] 176 | [PSCredential]$Credential, 177 | 178 | [Parameter(Mandatory, ParameterSetName = 'Session')] 179 | [System.Management.Automation.Runspaces.PSSession]$Session, 180 | 181 | [string]$DownloadPath = 'https://download.visualstudio.microsoft.com/download/pr/2d6bb6b2-226a-4baa-bdec-798822606ff1/8494001c276a4b96804cde7829c04d7f/ndp48-x86-x64-allos-enu.exe' 182 | ) 183 | # Install Dotnet 4.8 if required 184 | $RequiresDotnet = Invoke-Command -Session $Session -ScriptBlock { 185 | (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" -ErrorAction SilentlyContinue).Release -lt 528040 186 | } 187 | 188 | if ($RequiresDotnet) { 189 | Write-Host "Installing Dotnet 4.8 to '$($ComputerName)'" 190 | $DotnetInstall = Invoke-Command -Session $Session -ScriptBlock { 191 | $NetFx48Url = $using:DownloadPath 192 | $NetFx48Path = $env:TEMP 193 | $NetFx48InstallerFile = 'ndp48-x86-x64-allos-enu.exe' 194 | $NetFx48Installer = Join-Path $NetFx48Path $NetFx48InstallerFile 195 | if (!(Test-Path $NetFx48Installer)) { 196 | Write-Host "Downloading `'$NetFx48Url`' to `'$NetFx48Installer`'" 197 | (New-Object Net.WebClient).DownloadFile("$NetFx48Url","$NetFx48Installer") 198 | } 199 | 200 | $psi = New-Object System.Diagnostics.ProcessStartInfo 201 | $psi.WorkingDirectory = "$NetFx48Path" 202 | $psi.FileName = "$NetFx48InstallerFile" 203 | $psi.Arguments = "/q /norestart" 204 | 205 | Write-Host "Installing `'$NetFx48Installer`'" 206 | $s = [System.Diagnostics.Process]::Start($psi); 207 | $s.WaitForExit(); 208 | 209 | return $s.ExitCode 210 | } 211 | 212 | return $DotnetInstall 213 | } else { 214 | return 0 # No work performed / success 215 | } 216 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Deployment 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | ignore_cache: 7 | description: Does not restore a package cache if selected. 8 | required: false 9 | type: boolean 10 | 11 | vm_size: 12 | description: The size of VM to spin up. 13 | default: Standard_B4ms # Standard_B4as_v2 14 | 15 | images: 16 | description: The Azure images to test on. 17 | default: Win2022AzureEditionCore, Win2019Datacenter 18 | type: string 19 | 20 | variants: 21 | description: The configurations to test with. 22 | default: self-signed, single, wildcard 23 | 24 | pull_request_target: 25 | paths-ignore: 26 | - .github/workflows/** 27 | 28 | types: 29 | - opened 30 | - reopened 31 | - synchronize 32 | - ready_for_review 33 | 34 | # May want to remove this, as though it's neat to only have one job running per 35 | # ref, it may cancel before cleanup has happened. 36 | # concurrency: 37 | # group: ${{ github.workflow }}-${{ github.ref }} 38 | # cancel-in-progress: true 39 | 40 | defaults: 41 | run: 42 | shell: pwsh 43 | 44 | jobs: 45 | matrix: 46 | name: Generate Testing Parameters 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Generate Test Matrix 50 | id: test-matrix 51 | shell: pwsh 52 | run: | 53 | $EventType = '${{ github.event_name }}' 54 | $AuthHeaders = @{Headers = @{Authorization = 'Bearer ${{ secrets.GITHUB_TOKEN }}'}} 55 | $GitHubApi = '${{ github.api_url }}' 56 | switch ($EventType) { 57 | 'workflow_dispatch' { 58 | # We have been triggered manually. Run the inputs! 59 | $Images = (-split '${{ inputs.images }}').Trim(',;') 60 | $Variants = (-split '${{ inputs.variants }}').Trim(',;') 61 | } 62 | 'pull_request_target' { 63 | # This is a pull request. If it's from a known maintainer, run a test - otherwise, exit. 64 | $Trigger = Invoke-RestMethod "$GitHubApi/repos/${{ github.repository }}/collaborators/${{ github.actor }}/permission" @AuthHeaders 65 | 66 | if ($Trigger.Permission -in @("admin", "write")) { 67 | $Images = "Win2022AzureEditionCore", "Win2019Datacenter" 68 | $Variants = "self-signed", "single", "wildcard" 69 | } else { 70 | Write-Error "Action was triggered by '${{ github.actor }}', who has '$TriggerPermission': Cancelling build." 71 | exit 1 72 | } 73 | } 74 | } 75 | "images=$($Images | ConvertTo-Json -Compress -AsArray)" >> $env:GITHUB_OUTPUT 76 | "variants=$($Variants | ConvertTo-Json -Compress -AsArray)" >> $env:GITHUB_OUTPUT 77 | 78 | outputs: 79 | matrix-os: ${{ steps.test-matrix.outputs.images }} 80 | matrix-variant: ${{ steps.test-matrix.outputs.variants }} 81 | 82 | build: 83 | name: Build Package Cache 84 | needs: matrix 85 | if: success() 86 | runs-on: windows-latest 87 | steps: 88 | - name: Check out code 89 | uses: actions/checkout@v4 90 | with: 91 | ref: "refs/pull/${{ github.event.number }}/merge" 92 | 93 | - name: Create License File 94 | run: | 95 | if (-not (Test-Path C:\choco-setup\files)) { 96 | New-Item -Path C:\choco-setup\files -ItemType Junction -Value $PWD.Path 97 | } 98 | $LicenseDir = Join-Path $env:ChocolateyInstall 'license' 99 | if (-not (Test-Path $LicenseDir)) {$null = mkdir $LicenseDir} 100 | Set-Content -Path $LicenseDir\chocolatey.license.xml -Value $( 101 | [System.Text.Encoding]::UTF8.GetString( 102 | [System.Convert]::FromBase64String('${{ secrets.LICENSE }}') 103 | ) 104 | ) 105 | 106 | - name: Setup Package Cache 107 | uses: actions/cache@v4 108 | if: inputs.ignore_cache != true 109 | with: 110 | path: | 111 | C:/choco-setup/files/files/*.nupkg 112 | C:/choco-setup/files/files/*.zip 113 | !**/chocolatey-license.*.nupkg 114 | key: "${{ hashFiles('files/*.json') }}" 115 | 116 | - name: Begin Setup 117 | run: C:\choco-setup\files\OfflineInstallPreparation.ps1 118 | 119 | - name: Upload Built Artifact 120 | id: build-upload 121 | uses: actions/upload-artifact@v4 122 | with: 123 | name: choco-packages 124 | path: | 125 | C:\choco-setup\files\* 126 | !C:\choco-setup\files\.git* 127 | !chocolatey-license.*.nupkg 128 | !C:\choco-setup\files\files\chocolatey.license.xml 129 | 130 | outputs: 131 | artifact-url: ${{ steps.build-upload.outputs.artifact-url }} 132 | 133 | runner_test_deploy: 134 | strategy: 135 | matrix: 136 | os: ${{ fromJson(needs.matrix.outputs.matrix-os) }} 137 | variant: ${{ fromJson(needs.matrix.outputs.matrix-variant) }} 138 | fail-fast: false 139 | name: ${{ matrix.os }} with ${{ matrix.variant }} certificate 140 | runs-on: windows-latest 141 | needs: [build, matrix] 142 | if: success() 143 | steps: 144 | - name: Check out code 145 | uses: actions/checkout@v4 146 | with: 147 | ref: "refs/pull/${{ github.event.number }}/merge" 148 | 149 | - name: Login to Azure 150 | uses: azure/login@v2 151 | with: 152 | creds: ${{ secrets.AZURE_CREDENTIALS }} 153 | enable-AzPSSession: true 154 | 155 | - name: Deploy '${{ matrix.os }}' VM 156 | id: deploy-vm 157 | uses: azure/powershell@v2 158 | with: 159 | inlineScript: | 160 | Import-Module .\.github\workflows\Helpers.psm1 161 | 162 | $Location = 'eastus2' 163 | $ResourceGroupName = "qsg-testing" 164 | if ('${{ github.run_id }}' -ne "`$`{{ github.run_id }}") {$ResourceGroupName += '-${{ github.run_id }}'} 165 | 166 | if (-not (Get-AzResourceGroup -ResourceGroupName $ResourceGroupName -ErrorAction SilentlyContinue)) { 167 | $null = New-AzResourceGroup -ResourceGroupName $ResourceGroupName -Location $Location -Force -Tag @{ 168 | CreatedBy = '${{ github.triggering_actor }}' 169 | Ref = '${{ github.ref }}' 170 | Until = (Get-Date (Get-Date).AddHours(1) -F 'yyyy/MM/dd') 171 | Commit = '${{ github.sha }}' 172 | } 173 | } 174 | 175 | $VMArgs = @{ 176 | Image = '${{ matrix.os }}' 177 | Size = [string]::IsNullOrEmpty('${{ inputs.vm_size }}') ? 'Standard_B4as_v2' : '${{ inputs.vm_size }}' 178 | } 179 | 180 | try { 181 | $VM = New-TestVM -ResourceGroupName $ResourceGroupName @VMArgs 182 | 183 | # Set NSG to have access 184 | Request-WinRmAccessForTesting -ResourceGroupName $ResourceGroupName -VmName $VM.Name 185 | 186 | $Session = New-HoplessRemotingSession -ComputerName $Vm.FullyQualifiedDomainName -Credential $Vm.Credential 187 | } catch { 188 | if ($VM) { 189 | $VM | Remove-AzVm -AsJob -Force -Confirm:$false 190 | } 191 | 192 | # Try again... 193 | $VM = New-TestVM -ResourceGroupName $ResourceGroupName @VMArgs 194 | 195 | # Set NSG to have access 196 | Request-WinRmAccessForTesting -ResourceGroupName $ResourceGroupName -VmName $VM.Name 197 | 198 | $Session = New-HoplessRemotingSession -ComputerName $Vm.FullyQualifiedDomainName -Credential $Vm.Credential 199 | } 200 | 201 | # Windows Server 2019 may require Dotnet 4.8 to be installed, and have a reboot 202 | $DotnetInstall = Install-Dotnet -Session $Session 203 | if ($DotnetInstall -eq 1641 -or $DotnetInstall -eq 3010) { 204 | Write-Host ".NET Framework 4.8 was installed, but a reboot is required before using Chocolatey CLI." 205 | $Reboot = Restart-AzVm -ResourceGroupName $ResourceGroupName -Name $VM.Name 206 | if ($Reboot.Status -eq 'Succeeded') { 207 | Write-Host "Reboot was successful after $($Reboot.Endtime - $Reboot.Starttime)" 208 | } 209 | 210 | # Recreate the session 211 | $Session = New-HoplessRemotingSession -ComputerName $Vm.FullyQualifiedDomainName -Credential $Vm.Credential 212 | } 213 | 214 | try { 215 | $DownloadUrl = if ('${{ needs.build.outputs.artifact-url }}' -match 'https://github.com/(?.+)/(?.+)/actions/runs/(?\d+)/artifacts/(?\d+)') { 216 | "https://api.github.com/repos/$($Matches.Owner)/$($Matches.Repository)/actions/artifacts/$($Matches.ArtifactId)/zip" 217 | } else { 218 | '${{ needs.build.outputs.artifact-url }}' 219 | } 220 | Write-Host "Downloading Build Artifact '$DownloadUrl' to '$($VM.Name)' @$(Get-Date -Format o)" 221 | Invoke-Command -Session $Session -ScriptBlock { 222 | if (-not (Test-Path C:\choco-setup\files)) {$null = mkdir C:\choco-setup\files -Force} 223 | $ProgressPreference = "SilentlyContinue" 224 | $Response = Invoke-WebRequest -Uri $using:DownloadUrl -UseBasicParsing -Headers @{ 225 | Authorization = "Bearer ${{ secrets.GITHUB_TOKEN }}" 226 | Accept = "application/vnd.github+json" 227 | "X-GitHub-Api-Version" = "2022-11-28" 228 | } -OutFile C:\choco-setup\files.zip 229 | Expand-Archive -Path C:\choco-setup\files.zip -DestinationPath C:\choco-setup\files\ 230 | } 231 | } finally { 232 | Write-Host "Finished Downloading @$(Get-Date -Format o)" 233 | } 234 | 235 | Write-Host "Creating License File on '$($VM.Name)'" 236 | Invoke-Command -Session $Session -ScriptBlock { 237 | Set-Content -Path C:\choco-setup\files\files\chocolatey.license.xml -Value $( 238 | [System.Text.Encoding]::UTF8.GetString( 239 | [System.Convert]::FromBase64String('${{ secrets.LICENSE }}') 240 | ) 241 | ) 242 | } 243 | 244 | Write-Host "Setting up '${{ matrix.variant }}' Certificate" 245 | $Certificate = switch ('${{ matrix.variant }}') { 246 | 'self-signed' { 247 | Write-Host "Using a Self-Signed Certificate for '$($Vm.Name)'" 248 | $CertDetails = @{FQDN = $Vm.Name} 249 | @{} 250 | } 251 | 'single' { 252 | $CertDetails = Invoke-Command -Session $Session -ScriptBlock { 253 | $Cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new( 254 | [Convert]::FromBase64String('${{ secrets.SINGLE_CERT }}'), 255 | (ConvertTo-SecureString '${{ secrets.SINGLE_PASS }}' -AsPlainText -Force), 256 | ("Exportable", "PersistKeySet", "MachineKeySet") 257 | ) 258 | 259 | $Store = [System.Security.Cryptography.X509Certificates.X509Store]::new("TrustedPeople", "LocalMachine") 260 | $Store.Open("ReadWrite") 261 | $null = $Store.Add($Cert) 262 | 263 | Add-Content $env:windir\system32\drivers\etc\hosts -Value "127.0.0.1 $($Cert.Subject -replace '^CN=')" 264 | 265 | @{ 266 | Thumbprint = $Cert.Thumbprint 267 | FQDN = $Cert.Subject -replace '^CN=' 268 | } 269 | } 270 | Write-Host "Using Certificate with Thumbprint '$($Thumbprint)'" 271 | @{Thumbprint = $CertDetails.Thumbprint; CertificateDnsName = $CertDetails.FQDN} 272 | } 273 | 'wildcard' { 274 | $CertDetails = Invoke-Command -Session $Session -ScriptBlock { 275 | $Cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new( 276 | [Convert]::FromBase64String('${{ secrets.WILDCARD_CERT }}'), 277 | (ConvertTo-SecureString '${{ secrets.WILDCARD_PASS }}' -AsPlainText -Force), 278 | ("Exportable", "PersistKeySet", "MachineKeySet") 279 | ) 280 | 281 | $Store = [System.Security.Cryptography.X509Certificates.X509Store]::new("TrustedPeople", "LocalMachine") 282 | $Store.Open("ReadWrite") 283 | $null = $Store.Add($Cert) 284 | 285 | Add-Content $env:windir\system32\drivers\etc\hosts -Value "127.0.0.1 $($Cert.Subject -replace '^CN=\*',$env:ComputerName)" 286 | 287 | @{ 288 | Thumbprint = $Cert.Thumbprint 289 | FQDN = $Cert.Subject -replace '^CN=\*',$env:ComputerName 290 | } 291 | } 292 | Write-Host "Using Wildcard with Thumbprint '$($Thumbprint)'" 293 | @{Thumbprint = $CertDetails.Thumbprint; CertificateDnsName = $CertDetails.FQDN} 294 | } 295 | } 296 | 297 | try { 298 | Write-Host "Installing QuickStart Guide on '$($VM.Name)'" 299 | $DatabaseCredential = [PSCredential]::new( 300 | 'ccmdbuser', 301 | (ConvertTo-SecureString "$(New-Guid)" -AsPlainText -Force) 302 | ) 303 | $Timer = [System.Diagnostics.Stopwatch]::StartNew() 304 | Invoke-Command -Session $Session -ScriptBlock { 305 | C:\choco-setup\files\Initialize-C4bSetup.ps1 @using:Certificate -DatabaseCredential $using:DatabaseCredential 306 | } 307 | $Timer.Stop() 308 | "deployment-time=$($Timer.Elapsed)" >> $env:GITHUB_OUTPUT 309 | 310 | # Run Tests 311 | Write-Host "Running Verification Tests on '$($VM.Name)'" 312 | $RemotingArgs = @{ 313 | ComputerName = $VM.FullyQualifiedDomainName 314 | Credential = $VM.Credential 315 | UseSSL = $true 316 | SessionOption = New-PSSessionOption -SkipCACheck -SkipCNCheck 317 | } 318 | $TestResults = Invoke-Command -Session $Session -ScriptBlock { 319 | if (-not (Get-Module Pester -ListAvailable).Where{$_.Version -gt "5.0"}) { 320 | Write-Host "Installing Pester 5 to run validation tests" 321 | $chocoArgs = @('install', 'pester', '-y', '--source="ChocolateyInternal"', '--no-progress') 322 | & choco @chocoArgs | Write-Host 323 | } 324 | (Get-ChildItem C:\choco-setup\files\tests\ -Recurse -Filter *.tests.ps1).Fullname 325 | } | ForEach-Object { 326 | Invoke-Command @RemotingArgs -ScriptBlock { 327 | param( 328 | $Path 329 | ) 330 | Import-Module C:\choco-setup\files\modules\C4B-Environment 331 | $configuration = New-PesterConfiguration @{ 332 | Run = @{ 333 | Container = New-PesterContainer -Path $Path -Data @{ Fqdn = $using:CertDetails.FQDN } 334 | Passthru = $true 335 | } 336 | Output = @{ 337 | Verbosity = 'Detailed' 338 | } 339 | TestResult = @{ 340 | Enabled = $true 341 | OutputFormat = 'NUnitXml' 342 | OutputPath = "C:\choco-setup\test-results\${{ matrix.os }}-${{ matrix.variant }}-$((Split-Path $Path -Leaf) -replace '.tests.ps1$')-verification.results.xml" 343 | } 344 | } 345 | 346 | Invoke-Pester -Configuration $configuration 347 | } -ArgumentList $_ 348 | } 349 | } finally { 350 | Write-Host "Copying Results from '$($VM.Name)' @$(Get-Date -Format o)" 351 | if (-not (Test-Path .\logs\ -PathType Container)) {$null = mkdir .\logs\} 352 | Copy-Item -FromSession $Session -Path C:\choco-setup\logs\* -Destination .\logs\ -ErrorAction SilentlyContinue 353 | Copy-Item -FromSession $Session -Path C:\choco-setup\test-results\* -Destination .\logs\ -ErrorAction SilentlyContinue 354 | } 355 | azPSVersion: latest 356 | 357 | - name: Publish Test Results 358 | uses: EnricoMi/publish-unit-test-result-action/windows@v2 359 | if: success() 360 | with: 361 | check_name: C4bVerification-${{ matrix.os }}-${{ matrix.variant }} 362 | comment_mode: failures 363 | files: | 364 | logs\*-verification.results.xml 365 | github_token: ${{ secrets.GITHUB_TOKEN }} 366 | 367 | - name: Publish Log Files 368 | uses: actions/upload-artifact@v4 369 | if: success() || failure() 370 | with: 371 | name: choco-setup-logs-${{ matrix.os }}-${{ matrix.variant }} 372 | path: logs\*.txt 373 | 374 | cleanup: 375 | name: Cleanup Test Resources 376 | runs-on: ubuntu-latest 377 | needs: [build, runner_test_deploy] 378 | if: always() 379 | steps: 380 | - name: Login to Azure 381 | uses: azure/login@v2 382 | with: 383 | creds: ${{ secrets.AZURE_CREDENTIALS }} 384 | enable-AzPSSession: true 385 | 386 | - name: Destroy Remaining Resources 387 | uses: azure/powershell@v2 388 | with: 389 | inlineScript: | 390 | if ('${{ needs.build.outputs.artifact-url }}' -match 'https://github.com/(?.+)/(?.+)/actions/runs/(?\d+)/artifacts/(?\d+)') { 391 | $DeleteArgs = @{ 392 | Uri = "https://api.github.com/repos/$($Matches.Owner)/$($Matches.Repository)/actions/artifacts/$($Matches.ArtifactId)" 393 | Method = "DELETE" 394 | Headers = @{ 395 | Authorization = "Bearer ${{ secrets.GITHUB_TOKEN }}" 396 | Accept = "application/vnd.github+json" 397 | "X-GitHub-Api-Version" = "2022-11-28" 398 | } 399 | } 400 | Invoke-RestMethod @DeleteArgs -ErrorAction SilentlyContinue 401 | } 402 | 403 | $ResourceGroupName = "qsg-testing" 404 | if ('${{ github.run_id }}' -ne "`$`{{ github.run_id }}") {$ResourceGroupName += '-${{ github.run_id }}'} 405 | if (Get-AzResourceGroup -ResourceGroupName $ResourceGroupName -ErrorAction SilentlyContinue) { 406 | Remove-AzResourceGroup -ResourceGroupName $ResourceGroupName -Force 407 | } 408 | azPSVersion: latest 409 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.nupkg 2 | *.zip 3 | /scripts/ChocolateyInstall.ps1 4 | Chocolatey.License.xml 5 | */bcrypt.net.0.1.0/* -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This document outlines how to help with development of the Chocolatey Quickstart Guide, particularly around testing changes. 2 | 3 | ## Development 4 | 5 | When looking to make a change ensure any working branch is taken from the tip of `main`. You can do this with the following: 6 | 7 | ```powershell 8 | $ChocolateyUpstream = ((git remote -v) -match "github.com/chocolatey/choco-quickstart-scripts.git \(fetch\)$" -split "\t")[0] 9 | git fetch $ChocolateyUpstream 10 | git checkout -b $NewBranchName $ChocolateyUpstream/main 11 | ``` 12 | 13 | ### Development Testing 14 | 15 | You must test your changes before submitting a PR. 16 | 17 | You should test on a clean, supported operating system. 18 | 19 | > NB: To save time in repeated testing from a clean environment, you can run the OfflineInstallPreparation script in your repository and copy the files directory before copying. 20 | 21 | To test the quickstart environment: 22 | 23 | 1. Copy the repository directory over to `C:\choco-setup\files\` on the test machine. You do not need to copy the `.git` directory. 24 | 1. Open an elevated Windows PowerShell console. 25 | 1. Run `C:\choco-setup\files\Initialize-C4bSetup.ps1`, and continue through the guide steps as detailed in `README.md`. 26 | 1. Run `C:\choco-setup\files\Test-C4bSetup.ps1` and check that all tests pass. 27 | 28 | ## Testing a PR 29 | 30 | Changes in a PR must be tested before merging. In order to set things up for testing do the following in an elevated Windows PowerShell terminal: 31 | 32 | 1. Set `$env:CHOCO_QSG_BRANCH` to the PR ID or Branch Name to download. 33 | 1. Run Quickstart Guide as documented, in the same session. 34 | 35 | Example: 36 | 37 | ```powershell 38 | $env:CHOCO_QSG_BRANCH = "< Insert PR ID or Upstream BranchName Here >" 39 | 40 | Set-ExecutionPolicy Bypass -Scope Process -Force 41 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::tls12 42 | Invoke-RestMethod "https://ch0.co/qsg-go" | Invoke-Expression 43 | ``` 44 | 45 | 1. Perform each step of the Quickstart Guide, and make sure the changes you have attempted to make work appropriately. 46 | 1. Run `Test-C4bSetup.ps1` and check that all tests pass. 47 | 1. If everything looks OK, push your branch and create your Pull Request. 48 | 49 | ## Creating a PR 50 | 51 | Push your branch to a fork or repository, and create a new PR [here](https://github.com/chocolatey/choco-quickstart-scripts/compare). 52 | 53 | You should fill out the issue template as much as possible. 54 | 55 | ### Rebasing Your Branch 56 | 57 | If something else has been merged since you created your branch, you should rebase your branch onto the new tip of `main`. If you'd already pushed the branch, you may need to force-push the new history over the upstream version. You can do this as follows: 58 | 59 | ```powershell 60 | $ChocolateyUpstream = ((git remote -v) -match "github.com/chocolatey/choco-quickstart-scripts.git \(fetch\)$" -split "\t")[0] 61 | git fetch $ChocolateyUpstream 62 | git rebase $ChocolateyUpstream\main --autostash 63 | ``` -------------------------------------------------------------------------------- /Initialize-C4bSetup.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | C4B Quick-Start Guide initial bootstrap script 4 | 5 | .DESCRIPTION 6 | - Performs the following C4B Server initial bootstrapping 7 | - Install of Chocolatey 8 | - Prompt for C4B license, with validation 9 | - Script to help turn your C4B license into a Chocolatey package 10 | - Setup of local `choco-setup` directories 11 | - Download of Chocolatey packages required for setup 12 | #> 13 | [CmdletBinding(DefaultParameterSetName = 'Prepare')] 14 | param( 15 | # Full path to Chocolatey license file. 16 | # Accepts any file, and moves and renames it correctly. 17 | # You can either define this as a parameter, or 18 | # script will prompt you for it. 19 | # Script will also validate expiry. 20 | [Parameter(ParameterSetName = 'Install')] 21 | [string] 22 | $LicenseFile = $( 23 | if (Test-Path $PSScriptRoot\files\chocolatey.license.xml) { 24 | # Offline setup has been run, we should use that license. 25 | Join-Path $PSScriptRoot "files\chocolatey.license.xml" 26 | } elseif (Test-Path $env:ChocolateyInstall\license\chocolatey.license.xml) { 27 | # Chocolatey is already installed, we can use that license. 28 | Join-Path $env:ChocolateyInstall "license\chocolatey.license.xml" 29 | } else { 30 | # Prompt the user for the license. 31 | $Wshell = New-Object -ComObject Wscript.Shell 32 | $null = $Wshell.Popup('You will need to provide the license file location. Please select your Chocolatey License in the next file dialog.') 33 | $null = [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") 34 | $OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog 35 | $OpenFileDialog.initialDirectory = "$env:USERPROFILE\Downloads" 36 | $OpenFileDialog.filter = 'All Files (*.*)| *.*' 37 | $null = $OpenFileDialog.ShowDialog() 38 | 39 | $OpenFileDialog.filename 40 | } 41 | ), 42 | 43 | # Specify a credential used for the ChocolateyManagement DB user. 44 | # Only required in install mode for the CCM setup script. 45 | # If not populated, the script will prompt for credentials. 46 | [Parameter(ParameterSetName = 'Install')] 47 | [System.Management.Automation.PSCredential] 48 | $DatabaseCredential = $( 49 | if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseUser) { 50 | (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseUser 51 | } else { 52 | [PSCredential]::new( 53 | "chocodbuser", 54 | (ConvertTo-SecureString "$(New-Guid)-$(New-Guid)" -Force -AsPlainText) 55 | ) 56 | } 57 | ), 58 | 59 | # The certificate thumbprint that identifies the target SSL certificate in 60 | # the local machine certificate stores. 61 | # Only used in install mode for the SSL setup script. 62 | [Parameter(ParameterSetName = 'Install')] 63 | [ArgumentCompleter({ 64 | Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { 65 | [System.Management.Automation.CompletionResult]::new( 66 | $_.Thumbprint, 67 | $_.Thumbprint, 68 | "ParameterValue", 69 | ($_.Subject -replace "^CN=(?.+),?.*$", '${FQDN}') 70 | ) 71 | } 72 | })] 73 | [string] 74 | $Thumbprint = $( 75 | if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { 76 | (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint 77 | } else { 78 | Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { 79 | $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed 80 | } | Select-Object -ExpandProperty Thumbprint -First 1 81 | } 82 | ), 83 | 84 | # If using a wildcard certificate, provide a DNS name you want to use to access services secured by the certificate.\ 85 | [Parameter(ParameterSetName = 'Install')] 86 | [Alias("FQDN")] 87 | [string] 88 | $CertificateDnsName = $( 89 | if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertSubject) { 90 | (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertSubject 91 | } 92 | ), 93 | 94 | # If provided, shows all Chocolatey output. Otherwise, blissful quiet. 95 | [switch] 96 | $ShowChocoOutput, 97 | 98 | # The branch or Pull Request to download the C4B setup scripts from. 99 | # Defaults to main. 100 | [Alias('PR')] 101 | [string] 102 | $Branch = $env:CHOCO_QSG_BRANCH, 103 | 104 | # If provided, will skip launching the browser at the end of setup. 105 | [Parameter(ParameterSetName = 'Install')] 106 | [switch]$SkipBrowserLaunch 107 | ) 108 | if ($ShowChocoOutput) { 109 | $global:PSDefaultParameterValues["Invoke-Choco:InformationAction"] = "Continue" 110 | } 111 | 112 | $QsRepo = if ($Branch) { 113 | if ((Invoke-RestMethod -Uri "https://api.github.com/repos/chocolatey/choco-quickstart-scripts/branches").name -contains $Branch) { 114 | "https://api.github.com/repos/chocolatey/choco-quickstart-scripts/zipball/$Branch" 115 | } elseif ($PullRequest = Invoke-RestMethod -Uri "https://api.github.com/repos/chocolatey/choco-quickstart-scripts/pulls/$Branch" -ErrorAction SilentlyContinue) { 116 | $PullRequest.head.repo.archive_url -replace '{archive_format}', 'zipball' -replace '{/ref}', "/$($PullRequest.head.ref)" 117 | } else { 118 | Write-Error "'$($Branch)' is not a valid branch or pull request number. Please provide a valid branch or pull request number." 119 | } 120 | } else { 121 | "https://api.github.com/repos/chocolatey/choco-quickstart-scripts/zipball/main" 122 | } 123 | 124 | $DefaultEap, $ErrorActionPreference = $ErrorActionPreference, 'Stop' 125 | Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Initialize-C4bSetup-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" 126 | 127 | try { 128 | # Setup initial choco-setup directories 129 | Write-Host "Setting up initial directories in"$env:SystemDrive\choco-setup"" -ForegroundColor Green 130 | $ChocoPath = "$env:SystemDrive\choco-setup" 131 | $FilesDir = Join-Path $ChocoPath "files" 132 | $PkgsDir = Join-Path $FilesDir "files" 133 | $TempDir = Join-Path $ChocoPath "temp" 134 | $TestDir = Join-Path $ChocoPath "tests" 135 | $xmlDir = Join-Path $ChocoPath "clixml" 136 | 137 | @($ChocoPath, $FilesDir, $PkgsDir, $TempDir, $TestDir, $xmlDir) | ForEach-Object { 138 | $null = New-Item -Path $_ -ItemType Directory -Force -ErrorAction Stop 139 | } 140 | 141 | if (-not $PSScriptRoot -or $PSScriptRoot -ne $FilesDir) { 142 | # Download and extract C4B setup files from repo 143 | try { 144 | Invoke-WebRequest -Uri $QsRepo -UseBasicParsing -OutFile "$TempDir\choco-quickstart-scripts.zip" 145 | Expand-Archive "$TempDir\choco-quickstart-scripts.zip" $TempDir 146 | Copy-Item "$TempDir\*\*" $FilesDir -Recurse -Force 147 | } finally { 148 | Remove-Item "$TempDir" -Recurse -Force 149 | } 150 | } 151 | 152 | # Add the Module Path and Import Helper Functions 153 | if (-not (Get-Module C4B-Environment -ListAvailable)) { 154 | if ($env:PSModulePath.Split(';') -notcontains "$FilesDir\modules") { 155 | [Environment]::SetEnvironmentVariable("PSModulePath", "$env:PSModulePath;$FilesDir\modules" , "Machine") 156 | $env:PSModulePath = [Environment]::GetEnvironmentVariables("Machine").PSModulePath 157 | } 158 | } 159 | Import-Module C4B-Environment -Verbose:$false 160 | 161 | # Downloading all CCM setup packages below 162 | Write-Host "Downloading missing nupkg files to $($PkgsDir)." -ForegroundColor Green 163 | Write-Host "This will take some time. Feel free to get a tea or coffee." -ForegroundColor Green 164 | 165 | & $FilesDir\OfflineInstallPreparation.ps1 -LicensePath $LicenseFile 166 | 167 | # Kick off unattended running of remaining setup scripts, if we're running from a saved-script. 168 | if ($PSScriptRoot -or $PSCmdlet.ParameterSetName -eq 'Install') { 169 | Update-Clixml -Properties @{ 170 | InitialDeployment = Get-Date 171 | } 172 | 173 | if ($Thumbprint) { 174 | Set-ChocoEnvironmentProperty CertThumbprint $Thumbprint 175 | 176 | if ($CertificateDnsName) { 177 | Set-ChocoEnvironmentProperty CertSubject $CertificateDnsName 178 | } 179 | 180 | # Collect current certificate configuration 181 | $Certificate = Get-Certificate -Thumbprint $Thumbprint 182 | Copy-CertToStore -Certificate $Certificate 183 | 184 | $null = Test-CertificateDomain -Thumbprint $Thumbprint 185 | } elseif ($PSScriptRoot) { 186 | # We're going to be using a self-signed certificate 187 | if (-not $CertificateDnsName) { 188 | $CertificateDnsName = $env:ComputerName 189 | } 190 | 191 | $CertificateArgs = @{ 192 | CertStoreLocation = "Cert:\LocalMachine\My" 193 | KeyUsage = "KeyEncipherment", "DigitalSignature" 194 | DnsName = $CertificateDnsName 195 | NotAfter = (Get-Date).AddYears(10) 196 | } 197 | 198 | $Certificate = New-SelfSignedCertificate @CertificateArgs 199 | Copy-CertToStore -Certificate $Certificate 200 | 201 | $Thumbprint = $Certificate.Thumbprint 202 | 203 | Set-ChocoEnvironmentProperty CertThumbprint $Thumbprint 204 | Set-ChocoEnvironmentProperty CertSubject $CertificateDnsName 205 | } 206 | 207 | if ($DatabaseCredential) { 208 | Set-ChocoEnvironmentProperty DatabaseUser $DatabaseCredential 209 | } 210 | 211 | if (Test-Path $FilesDir\files\*.nupkg) { 212 | Invoke-Choco source add --name LocalChocolateySetup --source $FilesDir\files\ --Priority 1 213 | } 214 | 215 | # Set Choco Server Chocolatey Configuration 216 | Invoke-Choco feature enable --name="'excludeChocolateyPackagesDuringUpgradeAll'" 217 | Invoke-Choco feature enable --name="'usePackageHashValidation'" 218 | 219 | # Convert license to a "choco-license" package, and install it locally to test 220 | Write-Host "Creating a 'chocolatey-license' package, and testing install." -ForegroundColor Green 221 | Set-Location $FilesDir 222 | .\scripts\Create-ChocoLicensePkg.ps1 223 | Remove-Item "$env:SystemDrive\choco-setup\packaging" -Recurse -Force 224 | 225 | $Certificate = @{} 226 | if ($Thumbprint) { $Certificate.Thumbprint = $Thumbprint } 227 | 228 | Set-Location "$env:SystemDrive\choco-setup\files" 229 | .\Start-C4BNexusSetup.ps1 @Certificate 230 | .\Start-C4bCcmSetup.ps1 @Certificate -DatabaseCredential $DatabaseCredential 231 | .\Start-C4bJenkinsSetup.ps1 @Certificate 232 | 233 | Complete-C4bSetup -SkipBrowserLaunch:$SkipBrowserLaunch 234 | } 235 | } finally { 236 | $ErrorActionPreference = $DefaultEap 237 | Stop-Transcript 238 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /OfflineInstallPreparation.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Prepares the repository for an offline deployment. 4 | 5 | .Description 6 | These scripts can be run from a network without access to the internet, 7 | but it needs to prepare packages to be run offline. 8 | This script downloads and internalizes packages for such usage. 9 | 10 | .Notes 11 | This must be run on a Windows system with access to the internet because 12 | it uses Chocolatey for Business' Package Internalizer. 13 | 14 | .Notes 15 | Instead of using this script, you can internalize all required packages manually, 16 | zip them, and drop them in the files directory as shown below. 17 | 18 | .Example 19 | .\OfflineInstallPreparation.ps1 -LicensePath C:\ProgramData\chocolatey\license\chocolatey.license.xml 20 | #> 21 | [CmdletBinding()] 22 | param( 23 | [ValidateScript({ 24 | if (-not (Test-Path (Convert-Path $_))) { 25 | throw "License file does not exist at '$($_)'. Please provide a valid -LicensePath" 26 | } 27 | try { 28 | [xml]$License = Get-Content $_ 29 | $Expiry = Get-Date $License.license.expiration 30 | if (-not $Expiry -or $Expiry -lt (Get-Date)) {throw} 31 | } catch { 32 | throw "License '$($_)' is not valid.$(if ($Expiry) {" It expired at '$($Expiry)'."})" 33 | } 34 | $true 35 | })] 36 | [string]$LicensePath = $( 37 | if (Test-Path $PSScriptRoot\files\chocolatey.license.xml) { 38 | # Offline setup has been run, we should use that license. 39 | Join-Path $PSScriptRoot "files\chocolatey.license.xml" 40 | } elseif (Test-Path $env:ChocolateyInstall\license\chocolatey.license.xml) { 41 | # Chocolatey is already installed, we can use that license. 42 | Join-Path $env:ChocolateyInstall "license\chocolatey.license.xml" 43 | } else { 44 | # Prompt the user for the license. 45 | $Wshell = New-Object -ComObject Wscript.Shell 46 | $null = $Wshell.Popup('You will need to provide the license file location. Please select your Chocolatey License in the next file dialog.') 47 | $null = [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") 48 | $OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog 49 | $OpenFileDialog.initialDirectory = "$env:USERPROFILE\Downloads" 50 | $OpenFileDialog.filter = 'All Files (*.*)| *.*' 51 | $null = $OpenFileDialog.ShowDialog() 52 | 53 | $OpenFileDialog.filename 54 | } 55 | ), 56 | 57 | [string]$WorkingDirectory = $(Join-Path $env:Temp "choco-offline") 58 | ) 59 | $ErrorActionPreference = "Stop" 60 | $ProgressPreference = "SilentlyContinue" 61 | $LicensePath = Convert-Path $LicensePath 62 | 63 | $ChocoInstallScript = Join-Path $PSScriptRoot "scripts\ChocolateyInstall.ps1" 64 | if (-not (Test-Path $ChocoInstallScript)) { 65 | Invoke-WebRequest -Uri 'https://chocolatey.org/install.ps1' -OutFile $ChocoInstallScript 66 | } 67 | 68 | $Signature = Get-AuthenticodeSignature -FilePath $ChocoInstallScript 69 | 70 | if ($Signature.Status -eq 'Valid' -and $Signature.SignerCertificate.Subject -eq 'CN="Chocolatey Software, Inc", O="Chocolatey Software, Inc", L=Topeka, S=Kansas, C=US') { 71 | if (-not (Get-Command choco.exe -ErrorAction SilentlyContinue)) { 72 | if (Test-Path $PSScriptRoot\files\chocolatey.*.nupkg) { 73 | $env:ChocolateyDownloadUrl = (Convert-Path $PSScriptRoot\files\chocolatey.*.nupkg)[0] 74 | } 75 | & $ChocoInstallScript 76 | } 77 | } else { 78 | Write-Error "ChocolateyInstall.ps1 script signature is not valid. Please investigate." -ErrorAction Stop 79 | } 80 | 81 | Import-Module $PSScriptRoot\modules\C4B-Environment -Force 82 | 83 | # Initialize environment, ensure Chocolatey For Business, etc. 84 | $Licensed = ($($(choco.exe)[0] -match "^Chocolatey (?\S+)\s*(?Business)?$") -and $Matches.LicenseType) 85 | $InstalledLicensePath = "$env:ChocolateyInstall\license\chocolatey.license.xml" 86 | if (-not $Licensed) { 87 | if (-not (Test-Path $InstalledLicensePath)) { 88 | if (-not (Test-Path $env:ChocolateyInstall\license)) { 89 | $null = New-Item $env:ChocolateyInstall\license -ItemType Directory 90 | } 91 | Copy-Item $LicensePath $InstalledLicensePath -Force 92 | } 93 | $ExtensionSource = if (Test-Path $PSScriptRoot\files\chocolatey.extension.*.nupkg) { 94 | Convert-Path $PSScriptRoot\files\ 95 | } else { 96 | 'https://licensedpackages.chocolatey.org/api/v2/' 97 | } 98 | Invoke-Choco install chocolatey.extension --source $ExtensionSource --params="'/NoContextMenu'" --confirm 99 | } 100 | 101 | # Download each set of packages to the output directories 102 | $PackageWorkingDirectory = Join-Path $WorkingDirectory "Packages" 103 | if (-not (Test-Path $PackageWorkingDirectory)) { 104 | $null = New-Item -Path $PackageWorkingDirectory -ItemType Directory -Force 105 | } 106 | foreach ($Package in (Get-Content $PSScriptRoot\files\chocolatey.json | ConvertFrom-Json).packages) { 107 | $ChocoArgs = @( 108 | "download", "$($Package.Name)" 109 | "--output-directory", $PackageWorkingDirectory 110 | "--ignore-dependencies" 111 | "--no-progress" 112 | ) 113 | $ChocoArgs += switch ($Package.psobject.properties.name) { 114 | "Version" { "--version=$($Package.Version)" } 115 | "Args" { $Package.Args } 116 | } 117 | if ($Package.Internalize -or $Package.PSObject.Properties.Name -notcontains "Internalize") { 118 | $ChocoArgs += "--internalize" # Default to internalizing 119 | } 120 | 121 | try { 122 | if (-not (Get-ChocolateyPackageMetadata -Path $PackageWorkingDirectory -Id $Package.Name) -and -not (Get-ChocolateyPackageMetadata -Path "$PSScriptRoot\files\" -Id $Package.Name)) { 123 | Write-Host "Downloading '$($Package.Name)'" 124 | 125 | while ((Get-ChildItem $PackageWorkingDirectory -Filter *.nupkg).Where{$_.CreationTime -gt (Get-Date).AddMinutes(-1)}.Count -gt 5) { 126 | Write-Verbose "Slowing down for a minute, in order to not trigger rate-limiting..." 127 | Start-Sleep -Seconds 5 128 | } 129 | 130 | Invoke-Choco @ChocoArgs 131 | } 132 | } catch { 133 | throw $_ 134 | } 135 | } 136 | Move-Item -Path $PackageWorkingDirectory\*.nupkg -Destination $PSScriptRoot\files\ 137 | 138 | # Jenkins Plugins 139 | $PluginsWorkingDirectory = Join-Path $WorkingDirectory "JenkinsPlugins" 140 | if (-not (Test-Path $PluginsWorkingDirectory)) { 141 | $null = New-Item -Path $PluginsWorkingDirectory -ItemType Directory -Force 142 | } 143 | if (Test-Path $PSScriptRoot\files\JenkinsPlugins.zip) { 144 | Expand-Archive -Path $PSScriptRoot\files\JenkinsPlugins.zip -DestinationPath $PluginsWorkingDirectory -Force 145 | } 146 | $ProgressPreference = "Ignore" 147 | foreach ($Plugin in (Get-Content $PSScriptRoot\files\jenkins.json | ConvertFrom-Json).plugins) { 148 | $RestArgs = @{ 149 | Uri = "https://updates.jenkins-ci.org/latest/$($Plugin.Name).hpi" 150 | OutFile = Join-Path $PluginsWorkingDirectory "$($Plugin.Name).hpi" 151 | } 152 | if ($Plugin.Version -and $Plugin.Version -ne 'latest') { 153 | $RestArgs.Uri = "https://updates.jenkins-ci.org/download/plugins/$($Plugin.Name)/$($Plugin.Version)/$($Plugin.Name).hpi" 154 | } 155 | if (-not (Test-Path $RestArgs.OutFile)) { 156 | Invoke-WebRequest @RestArgs -UseBasicParsing 157 | } 158 | } 159 | Compress-Archive -Path $PluginsWorkingDirectory\* -Destination $PSScriptRoot\files\JenkinsPlugins.zip -Force 160 | 161 | # BCryptDll 162 | $null = Get-BcryptDll 163 | 164 | # License 165 | if ($LicensePath -ne "$PSScriptRoot\files\chocolatey.license.xml") { 166 | Copy-Item -Path (Convert-Path $LicensePath) -Destination $PSScriptRoot\files\chocolatey.license.xml 167 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # C4B Quick-Start Guide - Supporting Scripts 2 | 3 | This repository contains a set of supporting scripts used for the Chocolatey for Business (C4B) Quick-Start Guide (QSG). 4 | 5 | These scripts can be used to assist in setup of a brand new Windows Server as a C4B Server. 6 | 7 | Below is the Quick Start Guide as it exists currently on the [Chocolatey Docs](https://docs.chocolatey.org/en-us/guides/organizations/quick-start-guide/chocolatey-for-business-quick-start-guide). 8 | 9 | --- 10 | 11 | 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: 12 | 13 | - The Chocolatey Licensed components 14 | - A NuGet V3 Repository (Nexus) 15 | - Chocolatey Central Management (CCM) 16 | - An Automation Pipeline (Jenkins) 17 | 18 | > :memo: **NOTE** 19 | > 20 | > This quick-start guide is intended for customers who have recently purchased Chocolatey for Business (C4B), or are evaluating C4B as part of a proof-of-concept. 21 | > It is **opinionated**, and thus illustrates only one method of setting up your Chocolatey environment. 22 | > Our goal is to get you up-and-running smoothly and efficiently in order to fully test out the feature set. 23 | > For a more exhaustive reference of possible setup scenarios, you may refer to the [Organizational Deployment Documentation](https://docs.chocolatey.org/en-us/guides/organizations/organizational-deployment-guide). 24 | 25 | If you have any questions or would like to discuss more involved implementations, please feel free to reach out to your Chocolatey representative. 26 | 27 | Let's get started! 28 | 29 | ## Components 30 | 31 | Chocolatey for Business Server Components 32 | 33 | As illustrated in the diagram above, there are four main components to a Chocolatey for Business installation: 34 | 35 | 1. **C4B Licensed components**: A licensed version of Chocolatey includes: 36 | - Installation of the Chocolatey OSS client package itself (`chocolatey`) 37 | - Chocolatey license file (`chocolatey.license.xml`) installed in the correct directory (`ProgramData\chocolatey\license`) 38 | - Installation of the Chocolatey Licensed extension (`chocolatey.extension`), giving you access to features like Package Builder, Package Internalizer, etc. (full list [here](https://docs.chocolatey.org/en-us/features/)). 39 | 40 | 1. **NuGet V3 Repository Server App (Nexus)**: Chocolatey works best with a NuGet V3 repository. This application hosts and manages versioning of your Chocolatey package artifacts, in their enhanced NuGet package (.nupkg) file format. The quick start guide helps you setup [Sonatype Nexus Repository Manager (OSS)](https://www.sonatype.com/products/nexus-repository). 41 | 42 | 1. **Chocolatey Central Management (CCM)**: CCM is the Web UI portal for your entire Chocolatey environment. Your endpoints check-in to CCM to report their package status. This includes the Chocolatey packages they have installed, and whether any of these packages are outdated. And now, with CCM Deployments, you can also deploy packages or package updates to groups of endpoints, as well as ad-hoc PowerShell commands. CCM is backed by an MS SQL Database. This guide will set up MS SQL Express for you. 43 | 44 | 1. **Automation Pipeline (Jenkins)**: A pipeline tool will help you automate repetitive tasks, such checking for updates to a set of Chocolatey Packages from the Chocolatey Community Repository (CCR). If updates exist, the pipeline task will auto-internalize your list of packages, and push them into your NuGet repository for you. This guide will help you set up Jenkins as your automation pipeline. 45 | 46 | ## Requirements 47 | 48 | Below are the minimum requirements for setting up your C4B server via this guide: 49 | 50 | - Windows Server 2019+ (ideally, Windows Server 2022) 51 | - 4+ CPU cores (more preferred) 52 | - 16 GB+ RAM (8GB as a bare minimum; 4GB of RAM is reserved specifically for Nexus) 53 | - 500 GB+ of free space for local NuGet package artifact storage (more is better, and you may have to grow this as your packages and versions increase) 54 | - Open outgoing (egress) Internet access 55 | - Administrator user rights 56 | 57 | ## Installation 58 | 59 | ### Step 0: Preparation of C4B Server 60 | 61 | 1. Provision your C4B server on the infrastructure of your choice. 62 | 63 | 1. Install all Windows Updates. 64 | 65 | 1. If you plan on joining this server to your Active Directory domain, do so now before beginning setup below. 66 | 67 | 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. 68 | 69 | 1. Copy your `chocolatey.license.xml` license file (from the email you received) onto your C4B Server. 70 | 71 | > :warning:**DISCLAIMER**: This guide utilizes code from a GitHub repository, namely: [choco-quickstart-scripts](https://github.com/chocolatey/choco-quickstart-scripts). Though we explain what each script does in drop-down boxes, please do your due diligence to review this code and ensure it meets your Organizational requirements. 72 | 73 | > :memo:**Offline Install**: If your C4B server does not have unrestricted access to the internet, you can download the `choco-quickstart-scripts` repository to a Windows machine that is connected to the internet and run `OfflineInstallPreparation.ps1`. This will use Chocolatey to save all of the required assets into the repository folder, which can then be transferred to the target C4B server. 74 | > - Download [choco-quickstart-scripts by clicking here](https://github.com/chocolatey/choco-quickstart-scripts/archive/refs/heads/main.zip). 75 | > - Unzip it, e.g.: `Expand-Archive -Path ~\Downloads\choco-quickstart-scripts-main.zip -DestinationPath ~\Downloads\choco-quickstart-scripts`. 76 | > - Run the OfflineInstallPreparation script, e.g.: `~\Downloads\choco-quickstart-scripts\choco-quickstart-scripts-main\OfflineInstallPreparation.ps1`. 77 | > - Run the following to zip up your offline bundle: `Compress-Archive -Path ~\Downloads\choco-quickstart-scripts\choco-quickstart-scripts-main\* -DestinationPath ~\Downloads\C4B-Files.zip`. 78 | > - Transfer the `C4B-Files.zip` from your downloads folder to your offline system. All further steps will be carried out on the offline system. 79 | > - Run `Expand-Archive -Path \path\to\C4B-Files.zip -DestinationPath C:\choco-setup\files -Force` on your offline system. 80 | > - Proceed to Certificate Options! 81 | 82 | ### Step 1: Begin C4B Setup 83 | 84 | > :exclamation:**[IMPORTANT]** All commands must be run from an **elevated** Windows PowerShell window (and **not ISE**), by opening your PowerShell console with the `Run as Administrator` option. 85 | 86 | 1. Open a Windows PowerShell console with the `Run as Administrator` option, and paste and run the following code: 87 | 88 | ```powershell 89 | Set-ExecutionPolicy Bypass -Scope Process -Force 90 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::tls12 91 | Invoke-RestMethod https://ch0.co/qsg-go | Invoke-Expression 92 | ``` 93 | 94 | #### Certificate Options 95 | 96 | **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: 97 | 98 | ```powershell 99 | Set-Location "$env:SystemDrive\choco-setup\files" 100 | .\Initialize-C4bSetup.ps1 -Thumbprint '' 101 | ``` 102 | 103 | **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: 104 | 105 | ```powershell 106 | Set-Location "$env:SystemDrive\choco-setup\files" 107 | .\Initialize-C4bSetup.ps1 -Thumbprint '' -CertificateDnsName '' 108 | ``` 109 | 110 | For example, with a wildcard certificate with a thumbprint of `deee9b2fabb24bdaae71d82286e08de1` you wish to use `chocolatey.foo.org`, the following would be required: 111 | 112 | ```powershell 113 | Set-Location "$env:SystemDrive\choco-setup\files" 114 | .\Initialize-C4bSetup.ps1 -Thumbprint deee9b2fabb24bdaae71d82286e08de1 -CertificateDnsName chocolatey.foo.org 115 | ``` 116 | 117 | > :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. 118 | 119 | > :memo: **NOTE** 120 | > 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. 121 | 122 | **Self-Signed Certificate** - If you are running a bare-minimum proof of concept environment, you can generate a self-signed certificate and use that. 123 | 124 | ```powershell 125 | Set-Location "$env:SystemDrive\choco-setup\files" 126 | .\Initialize-C4bSetup.ps1 127 | ``` 128 | 129 | >
130 | > What does this script do? (click to expand) 131 | >
    132 | >
  • Installs Chocolatey client from https://community.chocolatey.org
  • 133 | >
  • Prompts for your C4B license file location, with validation
  • 134 | >
  • Converts your C4B license into a Chocolatey package
  • 135 | >
  • Configures local "choco-setup" directories
  • 136 | >
  • Downloads setup files from "choco-quickstart-scripts" GitHub repo
  • 137 | >
  • Downloads Chocolatey packages required for setup
  • 138 | >
139 | >
140 | 141 | #### Script: Nexus Setup 142 | 143 | As part of the C4B setup, we install and configure Sonatype Nexus Repository, which is used for hosting your Chocolatey packages internally: 144 | 145 | >
146 | > What does this script do? (click to expand) 147 | >
    148 | >
  • Installs Sonatype Nexus Repository Manager OSS instance
  • 149 | >
  • Cleans up all demo repositories on Nexus
  • 150 | >
  • Creates a "ChocolateyInternal" NuGet repository
  • 151 | >
  • Creates a "ChocolateyTest" NuGet repository
  • 152 | >
  • Creates a "choco-install" raw repository
  • 153 | >
  • Sets up "ChocolateyInternal" on C4B Server as source, with API key
  • 154 | >
  • Adds firewall rule for repository access
  • 155 | >
  • Installs MS Edge, as Internet Explorer cannot access the Sonatype Nexus site
  • 156 | >
  • Outputs data to a JSON file to pass between scripts
  • 157 | >
158 | >
159 | 160 | #### Script: Chocolatey Central Management Setup 161 | 162 | As part of the C4B setup, we install and configure Chocolatey Central Management and associated prerequisites to manage your Chocolatey for Business nodes: 163 | 164 | >
165 | > What does this script do? (click to expand) 166 | >
    167 | >
  • Installs MS SQL Express and SQL Server Management Studio (SSMS)
  • 168 | >
  • Creates "ChocolateyManagement" database, and adds appropriate `ChocoUser` permissions
  • 169 | >
  • Installs all 3 Chocolatey Central Management packages (database, service, web), with correct parameters
  • 170 | >
  • Outputs data to a JSON file to pass between scripts
  • 171 | >
172 | >
173 | 174 | #### Script: Jenkins Setup 175 | 176 | As part of the C4B setup, we install and configure Jenkins as a method for running Package Internalizer and other jobs: 177 | 178 | >
179 | > What does this script do? (click to expand) 180 | >
    181 | >
  • Installs Jenkins package
  • 182 | >
  • Updates Jenkins plugins
  • 183 | >
  • Configures pre-downloaded Jenkins scripts for Package Internalizer automation
  • 184 | >
  • Sets up pre-defined Jenkins jobs for the scripts above
  • 185 | >
186 | >
187 | 188 | #### Script: Complete Setup 189 | 190 | As part of the C4B setup, we create a readme and install the Chocolatey Agent on the server: 191 | 192 | >
193 | > What does this script do? (click to expand) 194 | >
    195 | >
  • Sets up Chocolatey Agent on this system
  • 196 | >
  • Writes a Readme.html file to the Public Desktop with account information for C4B services
  • 197 | >
  • Auto-opens README, CCM, Nexus, and Jenkins in your web browser
  • 198 | >
199 | >
200 | 201 | > :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. 202 | 203 | ### Step 2: Setting up Endpoints 204 | 205 | 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. 206 | 207 | 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: 208 | 209 | ```powershell 210 | Set-ExecutionPolicy Bypass -Scope Process -Force 211 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::tls12 212 | .\Register-C4bEndpoint.ps1 213 | ``` 214 | 215 | >
216 | > What does this script do? (click to expand) 217 | >
    218 | >
  • Installs Chocolatey client (chocolatey), using a script from your raw "choco-install" repository
  • 219 | >
  • Runs the "ClientSetup.ps1" script from your raw "choco-install" repository, which does the following:
  • 220 | >
  • Licenses Chocolatey by installing the license package (chocolatey-license) created during QDE setup
  • 221 | >
  • Installs the Chocolatey Licensed Extension (chocolatey.extension) without context menus
  • 222 | >
  • Configures "ChocolateyInternal" source
  • 223 | >
  • Disables access to the "chocolatey" public Chocolatey Community Repository (CCR)
  • 224 | >
  • Configures Self-Service mode and installs Chocolatey GUI (chocolateygui) along with its licensed extension (chocolateygui.extension)
  • 225 | >
  • Configures Central Management (CCM) check-in, and opts endpoints into CCM Deployments
  • 226 | >
227 | >
228 | 229 | #### Available parameters 230 | 231 | * `ClientCommunicationSalt` 232 | The salt for communication from an agent to an instance of Central Management Service. Details available in the README file on server desktop. 233 | * `ServiceCommunicationSalt` 234 | The salt for communication from an instance of Central Management Service to an agent. Details available in the README file on server desktop. 235 | * `RepositoryCredential` 236 | The credential to use to access the repository server from the endpoint. Details available in the README file on server desktop. 237 | * `ProxyUrl` 238 | The URL of a proxy server to use for connecting to the repository. 239 | * `ProxyCredential` 240 | The credentials, if required, to connect to the proxy server. 241 | * `IncludePackageTools` 242 | Install the Chocolatey Licensed Extension with right-click context menus available. 243 | * `AdditionalConfiguration` 244 | Allows for the application of user-defined configuration that is applied after the base configuration. 245 | * `AdditionalFeatures` 246 | Allows for the toggling of additional features that is applied after the base configuration. 247 | * `AdditionalPackages` 248 | Allows for the installation of additional packages after the system base packages have been installed. 249 | * `AdditionalSources` 250 | Allows for the addition of alternative sources after the base configuration has been applied. 251 | * `TrustCertificate` 252 | If passed, downloads the certificate from the client server before initializing Chocolatey Agent. 253 | 254 | #### Advanced Endpoint Configuration 255 | 256 | 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/). 257 | 258 | ### Conclusion 259 | 260 | Congratulations! If you followed all the steps detailed above, you should now have a fully functioning Chocolatey for Business implementation deployed in your environment. 261 | 262 | It is worth mentioning that some customers may have a more bespoke environment, with the presence of proxies and additional configuration management applications. Chocolatey is engineered to be quite flexible, specifically to account for these scenarios. Please refer to the many options for installation referenced on the [Installation page](https://docs.chocolatey.org/en-us/licensed-extension/setup#more-install-options). Again, If you have any questions or would like to discuss more involved implementations, please feel free to reach out to your Chocolatey representative. 263 | 264 | ### See it in Action 265 | 266 | If you'd prefer to watch and follow along, here is a recording of our Chocolatey Team going through this guide live on our Twitch stream: 267 | 268 | [YouTube Video](https://www.youtube.com/embed/qbIclPMEgig) 269 | -------------------------------------------------------------------------------- /Start-C4bCcmSetup.ps1: -------------------------------------------------------------------------------- 1 | #requires -modules C4B-Environment 2 | <# 3 | .SYNOPSIS 4 | C4B Quick-Start Guide CCM setup script 5 | 6 | .DESCRIPTION 7 | - Performs the following Chocolatey Central Management setup 8 | - Install of MS SQL Express 9 | - Creation and permissions of `ChocolateyManagement` DB 10 | - Install of all 3 CCM packages, with correct parameters 11 | #> 12 | [CmdletBinding()] 13 | param( 14 | # Credential used for the ChocolateyManagement DB user 15 | [Parameter()] 16 | [ValidateNotNull()] 17 | [System.Management.Automation.PSCredential] 18 | $DatabaseCredential = $( 19 | if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseUser) { 20 | (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseUser 21 | } else { 22 | [PSCredential]::new( 23 | "chocodbuser", 24 | (ConvertTo-SecureString "$(New-Guid)-$(New-Guid)" -Force -AsPlainText) 25 | ) 26 | } 27 | ), 28 | 29 | # Certificate to use for CCM service 30 | [Parameter()] 31 | [Alias('CertificateThumbprint')] 32 | [ArgumentCompleter({ 33 | Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { 34 | [System.Management.Automation.CompletionResult]::new( 35 | $_.Thumbprint, 36 | $_.Thumbprint, 37 | "ParameterValue", 38 | ($_.Subject -replace "^CN=(?.+),?.*$",'${FQDN}') 39 | ) 40 | } 41 | })] 42 | [ValidateScript({Test-CertificateDomain -Thumbprint $_})] 43 | [String] 44 | $Thumbprint = $( 45 | if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { 46 | (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint 47 | } else { 48 | Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { 49 | $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed 50 | } | Select-Object -ExpandProperty Thumbprint -First 1 51 | } 52 | ) 53 | ) 54 | process { 55 | $DefaultEap = $ErrorActionPreference 56 | $ErrorActionPreference = 'Stop' 57 | Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Start-C4bCcmSetup-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" 58 | 59 | $Packages = (Get-Content $PSScriptRoot\files\chocolatey.json | ConvertFrom-Json).packages 60 | 61 | Set-ChocoEnvironmentProperty -Name DatabaseUser -Value $DatabaseCredential 62 | 63 | # DB Setup 64 | Write-Host "Installing SQL Server Express" 65 | $chocoArgs = @('upgrade', 'sql-server-express', "--source='ChocolateyInternal'", '-y', '--no-progress') 66 | & Invoke-Choco @chocoArgs 67 | 68 | # https://docs.microsoft.com/en-us/sql/tools/configuration-manager/tcp-ip-properties-ip-addresses-tab 69 | Write-Verbose 'SQL Server: Configuring Remote Access on SQL Server Express.' 70 | $assemblyList = 'Microsoft.SqlServer.Management.Common', 'Microsoft.SqlServer.Smo', 'Microsoft.SqlServer.SqlWmiManagement', 'Microsoft.SqlServer.SmoExtended' 71 | 72 | foreach ($assembly in $assemblyList) { 73 | $assembly = [System.Reflection.Assembly]::LoadWithPartialName($assembly) 74 | } 75 | 76 | $wmi = New-Object Microsoft.SqlServer.Management.Smo.Wmi.ManagedComputer # connects to localhost by default 77 | $instance = $wmi.ServerInstances | Where-Object { $_.Name -eq 'SQLEXPRESS' } 78 | 79 | $np = $instance.ServerProtocols | Where-Object { $_.Name -eq 'Np' } 80 | $np.IsEnabled = $true 81 | $np.Alter() 82 | 83 | $tcp = $instance.ServerProtocols | Where-Object { $_.Name -eq 'Tcp' } 84 | $tcp.IsEnabled = $true 85 | $tcp.Alter() 86 | 87 | $tcpIpAll = $tcp.IpAddresses | Where-Object { $_.Name -eq 'IpAll' } 88 | 89 | $tcpDynamicPorts = $tcpIpAll.IpAddressProperties | Where-Object { $_.Name -eq 'TcpDynamicPorts' } 90 | $tcpDynamicPorts.Value = "" 91 | $tcp.Alter() 92 | 93 | $tcpPort = $tcpIpAll.IpAddressProperties | Where-Object { $_.Name -eq 'TcpPort' } 94 | $tcpPort.Value = "1433" 95 | $tcp.Alter() 96 | 97 | # This section will evaluate which version of SQL Express you have installed, and set a login value accordingly 98 | $SqlString = (Get-ChildItem -Path 'HKLM:\Software\Microsoft\Microsoft SQL Server').Name | 99 | Where-Object { $_ -like "HKEY_LOCAL_MACHINE\Software\Microsoft\Microsoft SQL Server\MSSQL*.SQLEXPRESS" } 100 | $SqlVersion = $SqlString.Split("\") | Where-Object { $_ -like "MSSQL*.SQLEXPRESS" } 101 | Write-Verbose 'SQL Server: Setting Mixed Mode Authentication.' 102 | $null = New-ItemProperty "HKLM:\Software\Microsoft\Microsoft SQL Server\$SqlVersion\MSSQLServer\" -Name 'LoginMode' -Value 2 -Force 103 | 104 | Write-Verbose "SQL Server: Forcing Restart of Instance." 105 | Restart-Service -Force 'MSSQL$SQLEXPRESS' 106 | 107 | Write-Verbose "SQL Server: Setting up SQL Server Browser and starting the service." 108 | Set-Service 'SQLBrowser' -StartupType Automatic 109 | Start-Service 'SQLBrowser' 110 | 111 | Write-Verbose "Firewall: Enabling SQLServer TCP port 1433." 112 | $null = netsh advfirewall firewall add rule name="SQL Server 1433" dir=in action=allow protocol=TCP localport=1433 profile=any enable=yes service=any 113 | #New-NetFirewallRule -DisplayName "Allow inbound TCP Port 1433" –Direction inbound –LocalPort 1433 -Protocol TCP -Action Allow 114 | 115 | Write-Verbose "Firewall: Enabling SQL Server browser UDP port 1434." 116 | $null = netsh advfirewall firewall add rule name="SQL Server Browser 1434" dir=in action=allow protocol=UDP localport=1434 profile=any enable=yes service=any 117 | #New-NetFirewallRule -DisplayName "Allow inbound UDP Port 1434" –Direction inbound –LocalPort 1434 -Protocol UDP -Action Allow 118 | 119 | # Install prerequisites for CCM 120 | Write-Host "Installing Chocolatey Central Management Prerequisites" 121 | $chocoArgs = @('install', 'IIS-WebServer', "--source='windowsfeatures'", '--no-progress', '-y') 122 | & Invoke-Choco @chocoArgs -ValidExitCodes 0, 3010 123 | 124 | $chocoArgs = @('install', 'IIS-ApplicationInit', "--source='windowsfeatures'" ,'--no-progress', '-y') 125 | & Invoke-Choco @chocoArgs -ValidExitCodes 0, 3010 126 | 127 | $chocoArgs = @('install', 'dotnet-aspnetcoremodule-v2', "--source='ChocolateyInternal'", "--version='$($Packages.Where{$_.Name -eq 'dotnet-aspnetcoremodule-v2'}.Version)'", '--no-progress', '-y') 128 | & Invoke-Choco @chocoArgs 129 | 130 | $chocoArgs = @('install', 'dotnet-8.0-runtime', "--source='ChocolateyInternal'", "--version=$($Packages.Where{$_.Name -eq 'dotnet-8.0-runtime'}.Version)", '--no-progress', '-y') 131 | & Invoke-Choco @chocoArgs 132 | 133 | $chocoArgs = @('install', 'dotnet-8.0-aspnetruntime', "--source='ChocolateyInternal'", "--version=$($Packages.Where{$_.Name -eq 'dotnet-8.0-aspnetruntime'}.Version)", '--no-progress', '-y') 134 | & Invoke-Choco @chocoArgs 135 | 136 | Write-Host "Creating Chocolatey Central Management Database" 137 | $chocoArgs = @('install', 'chocolatey-management-database', '--source="ChocolateyInternal"', '-y', '--package-parameters="''/ConnectionString=Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;Trusted_Connection=true;''"', '--no-progress') 138 | if ($PackageVersion = $Packages.Where{ $_.Name -eq 'chocolatey-management-database' }.Version) { 139 | $chocoArgs += "--version='$($PackageVersion)'" 140 | } 141 | & Invoke-Choco @chocoArgs 142 | 143 | # Add Local Windows User: 144 | $DatabaseUser = $DatabaseCredential.UserName 145 | $DatabaseUserPw = $DatabaseCredential.GetNetworkCredential().Password 146 | Add-DatabaseUserAndRoles -DatabaseName 'ChocolateyManagement' -Username $DatabaseUser -SqlUserPw $DatabaseUserPw -CreateSqlUser -DatabaseRoles @('db_datareader', 'db_datawriter') 147 | 148 | # Find FDQN for current machine 149 | $hostName = [System.Net.Dns]::GetHostName() 150 | $domainName = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().DomainName 151 | 152 | if (-not $hostName.EndsWith($domainName)) { 153 | $hostName += "." + $domainName 154 | } 155 | $CcmEndpoint = "http://$hostName" 156 | 157 | Write-Host "Installing Chocolatey Central Management Service" 158 | $chocoArgs = @('install', 'chocolatey-management-service', "--source='ChocolateyInternal'", '-y', "--package-parameters-sensitive=`"/ConnectionString:'Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;User ID=$DatabaseUser;Password=$DatabaseUserPw;'`"", '--no-progress') 159 | if ($PackageVersion = $Packages.Where{ $_.Name -eq 'chocolatey-management-service' }.Version) { 160 | $chocoArgs += "--version='$($PackageVersion)'" 161 | } 162 | if ($Thumbprint) { 163 | Write-Verbose "Validating certificate is in LocalMachine\TrustedPeople Store" 164 | if (-not (Get-Item Cert:\LocalMachine\TrustedPeople\$Thumbprint -EA 0) -and -not (Get-Item Cert:\LocalMachine\My\$Thumbprint -EA 0)) { 165 | Write-Warning "You specified $Thumbprint for use with CCM service, but the certificate is not in the required LocalMachine\TrustedPeople store!" 166 | Write-Warning "Please place certificate with thumbprint: $Thumbprint in the LocalMachine\TrustedPeople store and re-run this step" 167 | throw "Certificate not in correct location... exiting." 168 | } elseif ($MyCertificate = Get-Item Cert:\LocalMachine\My\$Thumbprint -EA 0) { 169 | Write-Verbose "Copying certificate from 'Personal' store to 'TrustedPeople'" 170 | Copy-CertToStore $MyCertificate 171 | } else { 172 | Write-Verbose "Certificate has been successfully found in correct store" 173 | } 174 | $chocoArgs += @("--package-parameters='/CertificateThumbprint=$Thumbprint'") 175 | } 176 | & Invoke-Choco @chocoArgs 177 | 178 | if (-not $MyCertificate) { $MyCertificate = Get-Item Cert:\LocalMachine\My\* } 179 | 180 | Write-Host "Installing Chocolatey Central Management Website" 181 | $chocoArgs = @('install', 'chocolatey-management-web', "--source='ChocolateyInternal'", '-y', "--package-parameters-sensitive=""'/ConnectionString:Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;User ID=$DatabaseUser;Password=$DatabaseUserPw;'""", '--no-progress') 182 | if ($PackageVersion = $Packages.Where{ $_.Name -eq 'chocolatey-management-web' }.Version) { 183 | $chocoArgs += "--version='$($PackageVersion)'" 184 | } 185 | & Invoke-Choco @chocoArgs 186 | 187 | # Setup Website SSL 188 | if ($Thumbprint) { 189 | Stop-CcmService 190 | Remove-CcmBinding 191 | New-CcmBinding -Thumbprint $Thumbprint 192 | Start-CcmService 193 | 194 | $CcmEndpoint = "https://$(Get-ChocoEnvironmentProperty CertSubject)" 195 | } 196 | choco config set centralManagementServiceUrl "$($CcmEndpoint):24020/ChocolateyManagementService" 197 | 198 | #Generate CCM Salt Values 199 | if (-not (Get-ChocoEnvironmentProperty ClientSalt)) { 200 | $ClientSaltValue = New-ServicePassword 201 | Set-ChocoEnvironmentProperty ClientSalt $ClientSaltValue 202 | } 203 | 204 | if (-not (Get-ChocoEnvironmentProperty ServiceSalt)) { 205 | $ServiceSaltValue = New-ServicePassword 206 | Set-ChocoEnvironmentProperty ServiceSalt $ServiceSaltValue 207 | } 208 | 209 | # Updating the Registration Script 210 | $EndpointScript = "$PSScriptRoot\scripts\Register-C4bEndpoint.ps1" 211 | Invoke-TextReplacementInFile -Path $EndpointScript -Replacement @{ 212 | "{{ ClientSaltValue }}" = Get-ChocoEnvironmentProperty ClientSalt -AsPlainText 213 | "{{ ServiceSaltValue }}" = Get-ChocoEnvironmentProperty ServiceSalt -AsPlainText 214 | "{{ FQDN }}" = Get-ChocoEnvironmentProperty CertSubject 215 | 216 | # Set a default value for TrustCertificate if we're using a self-signed cert 217 | '(?\s+\$TrustCertificate)(?\s*=\s*\$true)?(?,)?(?!\))' = "`${Parameter}$( 218 | if (Test-SelfSignedCertificate -Certificate $MyCertificate) {' = $true'} 219 | )`${Comma}" 220 | } 221 | 222 | # Create the site hosting the certificate import script on port 80 223 | if ($MyCertificate.NotAfter -gt (Get-Date).AddYears(5)) { 224 | .\scripts\New-IISCertificateHost.ps1 225 | } 226 | 227 | Wait-Site CCM 228 | 229 | Write-Host "Configuring Chocolatey Central Management" 230 | 231 | # Run initial configuration for CCM Admin 232 | if (-not ($CCMCredential = Get-ChocoEnvironmentProperty CCMCredential)) { 233 | $CCMCredential = [PSCredential]::new( 234 | "ccmadmin", 235 | (New-ServicePassword) 236 | ) 237 | Set-CcmAccountPassword -CcmEndpoint $CcmEndpoint -ConnectionString "Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;User ID=$DatabaseUser;Password=$DatabaseUserPw;" -NewPassword $CCMCredential.Password 238 | Set-ChocoEnvironmentProperty CCMCredential $CCMCredential 239 | } 240 | 241 | if (-not ($CCMEncryptionPassword = Get-ChocoEnvironmentProperty CCMEncryptionPassword)) { 242 | $CCMEncryptionPassword = New-ServicePassword 243 | 244 | Set-CcmEncryptionPassword -CcmEndpoint $CcmEndpoint -Credential $CCMCredential -NewPassword $CCMEncryptionPassword 245 | Set-ChocoEnvironmentProperty CCMEncryptionPassword $CCMEncryptionPassword 246 | } 247 | 248 | # Set Client and Service salts in Chocolatey Config 249 | Invoke-Choco config set centralManagementClientCommunicationSaltAdditivePassword $ClientSaltValue.ToPlainText() 250 | Invoke-Choco config set centralManagementServiceCommunicationSaltAdditivePassword $ServiceSaltValue.ToPlainText() 251 | 252 | # Set Website Root Address 253 | Update-CcmSettings -CcmEndpoint $CCmEndpoint -Credential $CCMCredential -Settings @{ 254 | website = @{ 255 | webSiteRootAddress = $CcmEndpoint 256 | } 257 | } 258 | 259 | $CcmSvcUrl = Invoke-Choco config get centralManagementServiceUrl -r 260 | Update-Clixml -Properties @{ 261 | CCMServiceURL = $CcmSvcUrl 262 | CCMWebPortal = "$CcmEndpoint/Account/Login" 263 | CCMDBUser = $DatabaseUser 264 | CCMInstallUser = whoami 265 | } 266 | 267 | Write-Host "Chocolatey Central Management Setup has now completed" -ForegroundColor Green 268 | 269 | $ErrorActionPreference = $DefaultEap 270 | Stop-Transcript 271 | } -------------------------------------------------------------------------------- /Start-C4bJenkinsSetup.ps1: -------------------------------------------------------------------------------- 1 | #requires -Modules C4B-Environment 2 | <# 3 | .SYNOPSIS 4 | C4B Quick-Start Guide Jenkins setup script 5 | 6 | .DESCRIPTION 7 | - Performs the following Jenkins setup 8 | - Install of Jenkins package 9 | - Silent upgrade of Jenkins plugins 10 | - Creation of Chocolatey-specific jobs from template files 11 | #> 12 | [CmdletBinding()] 13 | param( 14 | # Hostname of your C4B Server 15 | [string]$HostName = $env:ComputerName, 16 | 17 | # API key of your Nexus repo, for Chocolatey Jenkins jobs to use 18 | [string]$NuGetApiKey = $( 19 | if (-not (Get-Command Get-ChocoEnvironmentProperty -ErrorAction SilentlyContinue)) {. $PSScriptRoot\scripts\Get-Helpers.ps1} 20 | Get-ChocoEnvironmentProperty NuGetApiKey -AsPlainText 21 | ), 22 | 23 | # The certificate thumbprint that identifies the target SSL certificate in 24 | # the local machine certificate stores. 25 | [Parameter(ValueFromPipeline, ParameterSetName='Thumbprint')] 26 | [ArgumentCompleter({ 27 | Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { 28 | [System.Management.Automation.CompletionResult]::new( 29 | $_.Thumbprint, 30 | $_.Thumbprint, 31 | "ParameterValue", 32 | ($_.Subject -replace "^CN=(?.+),?.*$",'${FQDN}') 33 | ) 34 | } 35 | })] 36 | [ValidateScript({Test-CertificateDomain -Thumbprint $_})] 37 | [string] 38 | $Thumbprint = $( 39 | if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { 40 | (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint 41 | } else { 42 | Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { 43 | $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed 44 | } | Select-Object -ExpandProperty Thumbprint -First 1 45 | } 46 | ) 47 | ) 48 | process { 49 | $DefaultEap = $ErrorActionPreference 50 | $ErrorActionPreference = 'Stop' 51 | Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Start-C4bJenkinsSetup-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" 52 | 53 | $JenkinsScheme, $JenkinsPort = "http", "8080" 54 | 55 | # Install temurin21jre to meet JRE>11 dependency of Jenkins 56 | $chocoArgs = @('install', 'temurin21jre', "--source='ChocolateyInternal'", '-y', '--no-progress', "--params='/ADDLOCAL=FeatureJavaHome'") 57 | & Invoke-Choco @chocoArgs 58 | 59 | # Environment variable used to disable jenkins install login prompts 60 | [Environment]::SetEnvironmentVariable('JAVA_OPTS', '-Djenkins.install.runSetupWizard=false', 'Machine') 61 | 62 | # Install Jenkins 63 | Write-Host "Installing Jenkins" 64 | $chocoArgs = @('install', 'jenkins', "--source='ChocolateyInternal'", '-y', '--no-progress') 65 | & Invoke-Choco @chocoArgs 66 | 67 | # Jenkins needs a moment 68 | Wait-Site Jenkins 69 | 70 | # Disabling inital setup prompts 71 | $JenkinsHome = "C:\ProgramData\Jenkins\.jenkins" 72 | 73 | $JenkinsVersion = (choco.exe list jenkins --exact --limit-output).Split('|')[1] 74 | $JenkinsVersion | Out-File -FilePath $JenkinsHome\jenkins.install.UpgradeWizard.state -Encoding utf8 75 | $JenkinsVersion | Out-File -FilePath $JenkinsHome\jenkins.install.InstallUtil.lastExecVersion -Encoding utf8 76 | 77 | #region Set Jenkins Password 78 | $JenkinsCred = Set-JenkinsPassword -UserName 'admin' -NewPassword $(New-ServicePassword) -PassThru 79 | #endregion 80 | 81 | Stop-Service -Name Jenkins 82 | 83 | if ($Thumbprint) { 84 | $JenkinsScheme, $JenkinsPort = "https", 7443 85 | 86 | if ($SubjectWithoutCn = Get-ChocoEnvironmentProperty CertSubject) { 87 | $Hostname = $SubjectWithoutCn 88 | } 89 | 90 | # Generate Jenkins keystore 91 | Set-JenkinsCertificate -Thumbprint $Thumbprint -Port $JenkinsPort 92 | 93 | # Add firewall rule for Jenkins 94 | netsh advfirewall firewall add rule name="Jenkins-$($JenkinsPort)" dir=in action=allow protocol=tcp localport=$JenkinsPort 95 | } 96 | Set-JenkinsLocationConfiguration -Url "$($JenkinsScheme)://$($SubjectWithoutCn):$($JenkinsPort)" -Path $JenkinsHome\jenkins.model.JenkinsLocationConfiguration.xml 97 | 98 | #region Jenkins Plugin Install & Update 99 | $JenkinsPlugins = (Get-Content $PSScriptRoot\files\jenkins.json | ConvertFrom-Json).plugins 100 | 101 | if (Test-Path $PSScriptRoot\files\JenkinsPlugins.zip) { 102 | Expand-Archive -Path $PSScriptRoot\files\JenkinsPlugins.zip -DestinationPath $jenkinsHome\plugins\ -Force 103 | } 104 | 105 | # Performance is killed by Invoke-WebRequest's progress bars, turning them off to speed this up 106 | $ProgressPreference = 'SilentlyContinue' 107 | 108 | # Downloading Jenkins Plugins 109 | Write-Host "Downloading Jenkins Plugins" 110 | foreach ($Plugin in $JenkinsPlugins) { 111 | $PluginUri = if ($Plugin.Version -ne "latest") { 112 | 'https://updates.jenkins-ci.org/download/plugins/{0}/{1}/{0}.hpi' -f $Plugin.Name, $Plugin.Version 113 | } 114 | else { 115 | "https://updates.jenkins-ci.org/latest/$($Plugin.Name).hpi" 116 | } 117 | $PluginPath = '{0}/plugins/{1}.hpi' -f $jenkinsHome, $Plugin.Name 118 | if (-not (Test-Path $PluginPath)) { 119 | try { 120 | Invoke-WebRequest -Uri $PluginUri -OutFile $PluginPath -UseBasicParsing -ErrorAction Stop 121 | } 122 | catch { 123 | # We have internalized the required plugins for jobs we provide 124 | Write-Warning "Could not download '$($PluginName)' from '$($PluginUri)': $($_)" 125 | } 126 | } 127 | } 128 | 129 | # Restore default progress bar setting 130 | $ProgressPreference = 'Continue' 131 | #endregion 132 | 133 | #region Job Config 134 | $chocoArgs = @('install', 'chocolatey-licensed-jenkins-jobs', "--source='ChocolateyInternal'", '-y', '--no-progress') 135 | & Invoke-Choco @chocoArgs 136 | #endregion 137 | 138 | Write-Host "Starting Jenkins service back up" -ForegroundColor Green 139 | Start-Service -Name Jenkins 140 | 141 | # Save useful params 142 | Update-Clixml -Properties @{ 143 | JenkinsUri = "$($JenkinsScheme)://$($HostName):$($JenkinsPort)" 144 | JenkinsCredential = $JenkinsCred 145 | } 146 | 147 | Write-Host 'Jenkins setup complete' -ForegroundColor Green 148 | 149 | $ErrorActionPreference = $DefaultEap 150 | Stop-Transcript 151 | } -------------------------------------------------------------------------------- /Start-C4bNexusSetup.ps1: -------------------------------------------------------------------------------- 1 | #requires -Modules C4B-Environment 2 | <# 3 | .SYNOPSIS 4 | C4B Quick-Start Guide Nexus setup script 5 | 6 | .DESCRIPTION 7 | - Performs the following Sonatype Nexus Repository setup 8 | - Install of Sonatype Nexus Repository Manager OSS instance 9 | - Edit configuration to allow running of scripts 10 | - Cleanup of all demo source repositories 11 | - Creates `ChocolateyInternal` NuGet repository 12 | - Creates `ChocolateyTest` NuGet repository 13 | - Creates `choco-install` raw repository, with a script for offline Chocolatey install 14 | - Setup of `ChocolateyInternal` on C4B Server as source, with API key 15 | - Setup of firewall rule for repository access 16 | #> 17 | [CmdletBinding()] 18 | param( 19 | # The certificate thumbprint that identifies the target SSL certificate in 20 | # the local machine certificate stores. 21 | [Parameter()] 22 | [ArgumentCompleter({ 23 | Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { 24 | [System.Management.Automation.CompletionResult]::new( 25 | $_.Thumbprint, 26 | $_.Thumbprint, 27 | "ParameterValue", 28 | ($_.Subject -replace "^CN=(?.+),?.*$",'${FQDN}') 29 | ) 30 | } 31 | })] 32 | [ValidateScript({Test-CertificateDomain -Thumbprint $_})] 33 | [string] 34 | $Thumbprint = $( 35 | if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { 36 | (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint 37 | } else { 38 | Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { 39 | $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed 40 | } | Select-Object -ExpandProperty Thumbprint -First 1 41 | } 42 | ) 43 | ) 44 | process { 45 | $DefaultEap = $ErrorActionPreference 46 | $ErrorActionPreference = 'Stop' 47 | Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Start-C4bNexusSetup-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" 48 | $NexusPort = 8081 49 | 50 | $Packages = (Get-Content $PSScriptRoot\files\chocolatey.json | ConvertFrom-Json).packages 51 | 52 | # Install base nexus-repository package 53 | Write-Host "Installing Sonatype Nexus Repository" 54 | $chocoArgs = @('install', 'nexus-repository', '-y' ,'--no-progress', "--package-parameters='/Fqdn:localhost'") 55 | & Invoke-Choco @chocoArgs 56 | 57 | $chocoArgs = @('install', 'nexushell', '-y' ,'--no-progress') 58 | & Invoke-Choco @chocoArgs 59 | 60 | if ($Thumbprint) { 61 | $NexusPort = 8443 62 | 63 | $null = Set-NexusCert -Thumbprint $Thumbprint -Port $NexusPort 64 | 65 | if ($CertificateDnsName = Get-ChocoEnvironmentProperty CertSubject) { 66 | # Override the domain, so we don't get prompted for wildcard certificates 67 | Get-NexusLocalServiceUri -HostnameOverride $CertificateDnsName | Write-Verbose 68 | } 69 | } 70 | 71 | # Add Nexus port access via firewall 72 | $FwRuleParams = @{ 73 | DisplayName = "Nexus Repository access on TCP $NexusPort" 74 | Direction = 'Inbound' 75 | LocalPort = $NexusPort 76 | Protocol = 'TCP' 77 | Action = 'Allow' 78 | } 79 | $null = New-NetFirewallRule @FwRuleParams 80 | 81 | Wait-Site Nexus 82 | 83 | Write-Host "Configuring Sonatype Nexus Repository" 84 | 85 | # Build Credential Object, Connect to Nexus 86 | if (-not ($Credential = Get-ChocoEnvironmentProperty NexusCredential)) { 87 | Write-Host "Setting up admin account." 88 | $NexusDefaultPasswordPath = 'C:\programdata\sonatype-work\nexus3\admin.password' 89 | 90 | $Timeout = [System.Diagnostics.Stopwatch]::StartNew() 91 | while (-not (Test-Path $NexusDefaultPasswordPath) -and $Timeout.Elapsed.TotalMinutes -lt 3) { 92 | Start-Sleep -Seconds 5 93 | } 94 | 95 | $DefaultNexusCredential = [System.Management.Automation.PSCredential]::new( 96 | 'admin', 97 | (Get-Content $NexusDefaultPasswordPath | ConvertTo-SecureString -AsPlainText -Force) 98 | ) 99 | 100 | try { 101 | Connect-NexusServer -LocalService -Credential $DefaultNexusCredential -ErrorAction Stop 102 | 103 | $Credential = [PSCredential]::new( 104 | "admin", 105 | (New-ServicePassword) 106 | ) 107 | 108 | Set-NexusUserPassword -Username admin -NewPassword $Credential.Password -ErrorAction Stop 109 | Set-ChocoEnvironmentProperty -Name NexusCredential -Value $Credential 110 | } finally {} 111 | 112 | if (Test-Path $NexusDefaultPasswordPath) { 113 | Remove-Item -Path $NexusDefaultPasswordPath 114 | } 115 | } 116 | Connect-NexusServer -LocalService -Credential $Credential 117 | 118 | # Disable anonymous authentication 119 | $null = Set-NexusAnonymousAuth -Disabled 120 | 121 | # Drain default repositories 122 | $null = Get-NexusRepository | Where-Object Name -NotLike "choco*" | Remove-NexusRepository -Force 123 | 124 | # Enable NuGet Auth Realm 125 | Enable-NexusRealm -Realm 'NuGet API-Key Realm' 126 | 127 | # Create Chocolatey repositories 128 | if (-not (Get-NexusRepository -Name ChocolateyInternal)) { 129 | New-NexusNugetHostedRepository -Name ChocolateyInternal -DeploymentPolicy Allow 130 | } 131 | 132 | if (-not (Get-NexusRepository -Name ChocolateyTest)) { 133 | New-NexusNugetHostedRepository -Name ChocolateyTest -DeploymentPolicy Allow 134 | } 135 | 136 | if (-not (Get-NexusRepository -Name choco-install)) { 137 | New-NexusRawHostedRepository -Name choco-install -DeploymentPolicy Allow -ContentDisposition Attachment 138 | } 139 | 140 | # Create role for end user to pull from Nexus 141 | if (-not ($NexusRole = Get-NexusRole -Role 'chocorole' -ErrorAction SilentlyContinue)) { 142 | # Create Nexus role 143 | $RoleParams = @{ 144 | Id = "chocorole" 145 | Name = "chocorole" 146 | Description = "Role for web enabled choco clients" 147 | Privileges = @('nx-repository-view-nuget-*-browse', 'nx-repository-view-nuget-*-read', 'nx-repository-view-raw-*-read', 'nx-repository-view-raw-*-browse') 148 | } 149 | $NexusRole = New-NexusRole @RoleParams 150 | 151 | $Timeout = [System.Diagnostics.Stopwatch]::StartNew() 152 | while ($Timeout.Elapsed.TotalSeconds -lt 30 -and -not (Get-NexusRole -Role $RoleParams.Id -ErrorAction SilentlyContinue)) { 153 | Start-Sleep -Seconds 3 154 | } 155 | } 156 | 157 | # Create new user for endpoints 158 | if (-not (Get-NexusUser -User 'chocouser' -ErrorAction SilentlyContinue)) { 159 | # Create Nexus user 160 | $UserParams = @{ 161 | Username = 'chocouser' 162 | Password = New-ServicePassword 163 | FirstName = 'Choco' 164 | LastName = 'User' 165 | EmailAddress = 'chocouser@example.com' 166 | Status = 'Active' 167 | Roles = $NexusRole.Id 168 | } 169 | $null = New-NexusUser @UserParams 170 | 171 | $Timeout = [System.Diagnostics.Stopwatch]::StartNew() 172 | while ($Timeout.Elapsed.TotalSeconds -lt 30 -and -not (Get-NexusUser -User $UserParams.Username -ErrorAction SilentlyContinue)) { 173 | Start-Sleep -Seconds 3 174 | } 175 | 176 | Set-ChocoEnvironmentProperty ChocoUserPassword $UserParams.Password 177 | } 178 | 179 | # Create role for task runner to push to Nexus 180 | if (-not ($PackageUploadRole = Get-NexusRole -Role "package-uploader" -ErrorAction SilentlyContinue)) { 181 | $PackageUploadRole = New-NexusRole -Name "package-uploader" -Id "package-uploader" -Description "Role allowed to push and list packages" -Privileges @( 182 | "nx-repository-view-nuget-*-edit" 183 | "nx-repository-view-nuget-*-read" 184 | "nx-apikey-all" 185 | ) 186 | } 187 | 188 | # 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 189 | if ($UploadUser = Get-ChocoEnvironmentProperty PackageUploadCredential) { 190 | Write-Verbose "Using existing PackageUpload credential '$($UploadUser.UserName)'" 191 | } else { 192 | $UploadUser = [PSCredential]::new( 193 | 'chocoPackager', 194 | (New-ServicePassword -Length 64) 195 | ) 196 | } 197 | 198 | if (-not (Get-NexusUser -User $UploadUser.UserName)) { 199 | $NewUser = @{ 200 | Username = $UploadUser.UserName 201 | Password = $UploadUser.Password 202 | FirstName = "Chocolatey" 203 | LastName = "Packager" 204 | EmailAddress = "packager@$env:ComputerName.local" 205 | Status = "Active" 206 | Roles = $PackageUploadRole.Id 207 | } 208 | $null = New-NexusUser @NewUser 209 | 210 | Set-ChocoEnvironmentProperty -Name PackageUploadCredential -Value $UploadUser 211 | } 212 | 213 | # Retrieve the API Key to use in Jenkins et al 214 | if ($NuGetApiKey = Get-ChocoEnvironmentProperty PackageApiKey) { 215 | Write-Verbose "Using existing Nexus Api Key for '$($UploadUser.UserName)'" 216 | } else { 217 | $NuGetApiKey = (Get-NexusNuGetApiKey -Credential $UploadUser).apiKey 218 | Set-ChocoEnvironmentProperty -Name PackageApiKey -Value $NuGetApiKey 219 | } 220 | 221 | # Push latest ChocolateyInstall.ps1 to raw repo 222 | $ScriptDir = "$env:SystemDrive\choco-setup\files\scripts" 223 | $ChocoInstallScript = "$ScriptDir\ChocolateyInstall.ps1" 224 | 225 | if (-not (Test-Path $ChocoInstallScript)) { 226 | Invoke-WebRequest -Uri 'https://chocolatey.org/install.ps1' -OutFile $ChocoInstallScript 227 | } 228 | 229 | $Signature = Get-AuthenticodeSignature -FilePath $ChocoInstallScript 230 | 231 | if ($Signature.Status -eq 'Valid' -and $Signature.SignerCertificate.Subject -eq 'CN="Chocolatey Software, Inc", O="Chocolatey Software, Inc", L=Topeka, S=Kansas, C=US') { 232 | $null = New-NexusRawComponent -RepositoryName 'choco-install' -File $ChocoInstallScript 233 | } else { 234 | Write-Error "ChocolateyInstall.ps1 script signature is not valid. Please investigate." 235 | } 236 | 237 | # Push ClientSetup.ps1 to raw repo 238 | $ClientScript = "$PSScriptRoot\scripts\ClientSetup.ps1" 239 | (Get-Content -Path $ClientScript) -replace "{{hostname}}", "$((Get-NexusLocalServiceUri) -replace '^https?:\/\/')" | Set-Content -Path ($TemporaryFile = New-TemporaryFile).FullName 240 | $null = New-NexusRawComponent -RepositoryName 'choco-install' -File $TemporaryFile.FullName -Name "ClientSetup.ps1" 241 | 242 | # Nexus NuGet V3 Compatibility 243 | Invoke-Choco feature disable --name="'usePackageRepositoryOptimizations'" 244 | 245 | # Add ChocolateyInternal as a source repository 246 | $LocalSource = "$((Get-NexusRepository -Name 'ChocolateyInternal').url)/index.json" 247 | Invoke-Choco source add -n 'ChocolateyInternal' -s $LocalSource -u="$($UploadUser.UserName)" -p="$($UploadUser.GetNetworkCredential().Password)" --priority 1 248 | 249 | # Add ChocolateyTest as a source repository, to enable authenticated pushing 250 | Invoke-Choco source add -n 'ChocolateyTest' -s "$((Get-NexusRepository -Name 'ChocolateyTest').url)/index.json" -u="$($UploadUser.UserName)" -p="$($UploadUser.GetNetworkCredential().Password)" 251 | Invoke-Choco source disable -n 'ChocolateyTest' 252 | 253 | # Push all packages from previous steps to NuGet repo 254 | Write-Host "Pushing C4B Environment Packages to ChocolateyInternal" 255 | Get-ChildItem -Path "$env:SystemDrive\choco-setup\files\files" -Filter *.nupkg | ForEach-Object { 256 | Invoke-Choco push $_.FullName --source $LocalSource --apikey $NugetApiKey --force 257 | } 258 | 259 | # Temporary workaround to reset the NuGet v3 cache, such that it doesn't capture localhost as the FQDN 260 | Remove-NexusRepositoryFolder -RepositoryName ChocolateyInternal -Name v3 261 | 262 | # Remove Local Chocolatey Setup Source 263 | $chocoArgs = @('source', 'remove', '--name="LocalChocolateySetup"') 264 | & Invoke-Choco @chocoArgs 265 | 266 | # Install a non-IE browser for browsing the Nexus web portal. 267 | if (-not (Test-Path 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe')) { 268 | Write-Host "Installing Microsoft Edge, to allow viewing the Nexus site" 269 | Invoke-Choco install microsoft-edge -y --source ChocolateyInternal 270 | } 271 | $Key = @{ Key = 'HKLM:\Software\Policies\Microsoft\Edge' ; Value = 'HideFirstRunExperience' } 272 | if (-not (Test-Path $Key.Key)) { 273 | $null = New-Item -Path $Key.Key -Force 274 | } 275 | $null = New-ItemProperty -Path $Key.Key -Name $Key.Value -Value 1 -PropertyType DWORD -Force 276 | 277 | # Save useful params 278 | Update-Clixml -Properties @{ 279 | NexusUri = Get-NexusLocalServiceUri 280 | NexusCredential = $Credential 281 | NexusRepo = "$((Get-NexusRepository -Name 'ChocolateyInternal').url)/index.json" 282 | NuGetApiKey = $NugetApiKey | ConvertTo-SecureString -AsPlainText -Force 283 | } 284 | 285 | $ErrorActionPreference = $DefaultEap 286 | Stop-Transcript 287 | } -------------------------------------------------------------------------------- /Switch-SslSecurity.ps1: -------------------------------------------------------------------------------- 1 | #requires -modules C4B-Environment 2 | using namespace System.Net.Sockets 3 | using namespace System.Net.Security 4 | using namespace System.Security.Cryptography.X509Certificates 5 | <# 6 | .SYNOPSIS 7 | Generates or retrieves certificates and generates SSL bindings for 8 | Central Management and Nexus. 9 | 10 | .DESCRIPTION 11 | Removes any existing certificates which have the subject "chocoserver" to avoid 12 | issues, and then either generates or retrieves the required certificate. The 13 | certificate is placed in the required local machine store, and then the script 14 | generates SSL bindings for both Nexus and the Central Management website using the 15 | certificate. 16 | #> 17 | [CmdletBinding(DefaultParameterSetName='SelfSigned')] 18 | [OutputType([string])] 19 | param( 20 | # The certificate thumbprint that identifies the target SSL certificate in 21 | # the local machine certificate stores. 22 | # Ignored if supplied alongside -Subject. 23 | [Parameter(ValueFromPipeline, ParameterSetName='Thumbprint')] 24 | [ArgumentCompleter({ 25 | Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { 26 | [System.Management.Automation.CompletionResult]::new( 27 | $_.Thumbprint, 28 | $_.Thumbprint, 29 | "ParameterValue", 30 | ($_.Subject -replace "^CN=(?.+),?.*$",'${FQDN}') 31 | ) 32 | } 33 | })] 34 | [ValidateScript({Test-CertificateDomain -Thumbprint $_})] 35 | [string] 36 | $Thumbprint = $( 37 | if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { 38 | (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint 39 | } else { 40 | Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { 41 | $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed 42 | } | Select-Object -ExpandProperty Thumbprint -First 1 43 | } 44 | ), 45 | 46 | # The certificate subject that identifies the target SSL certificate in 47 | # the local machine certificate stores. 48 | [Parameter(ParameterSetName='Subject')] 49 | [string] 50 | $Subject, 51 | 52 | # If using a wildcard certificate, provide a DNS name you want to use to access services secured by the certificate. 53 | [Parameter(ParameterSetName='Subject')] 54 | [Parameter(ParameterSetName='Thumbprint')] 55 | [string] 56 | $CertificateDnsName = $( 57 | if (-not (Get-Command Get-ChocoEnvironmentProperty -ErrorAction SilentlyContinue)) {. $PSScriptRoot\scripts\Get-Helpers.ps1} 58 | Get-ChocoEnvironmentProperty CertSubject 59 | ), 60 | 61 | # API key of your Nexus repo, to add to the source setup on C4B Server. 62 | [string]$NuGetApiKey = $( 63 | if (-not (Get-Command Get-ChocoEnvironmentProperty -ErrorAction SilentlyContinue)) {. $PSScriptRoot\scripts\Get-Helpers.ps1} 64 | Get-ChocoEnvironmentProperty NuGetApiKey -AsPlainText 65 | ) 66 | ) 67 | process { 68 | $DefaultEap = $ErrorActionPreference 69 | $ErrorActionPreference = 'Stop' 70 | Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Switch-SslSecurity-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" 71 | 72 | # Collect current certificate configuration 73 | $Certificate = if ($Subject) { 74 | Get-Certificate -Subject $Subject 75 | } elseif ($Thumbprint) { 76 | Get-Certificate -Thumbprint $Thumbprint 77 | } 78 | 79 | if (-not $CertificateDnsName -and -not ($CertificateDnsName = Get-ChocoEnvironmentProperty CertSubject)) { 80 | $null = Test-CertificateDomain -Thumbprint $Certificate.Thumbprint 81 | } else { 82 | $SubjectWithoutCn = $CertificateDnsName 83 | } 84 | 85 | <# Nexus #> 86 | # Stop Services/Processes/Websites required 87 | Stop-Service nexus 88 | 89 | # Put certificate in TrustedPeople 90 | Copy-CertToStore -Certificate $Certificate 91 | 92 | # Generate Nexus keystore 93 | $null = Set-NexusCert -Thumbprint $Certificate.Thumbprint 94 | 95 | # Add firewall rule for Nexus 96 | netsh advfirewall firewall add rule name="Nexus-8443" dir=in action=allow protocol=tcp localport=8443 97 | 98 | Write-Verbose "Starting up Nexus" 99 | Start-Service nexus 100 | 101 | Write-Warning "Waiting to give Nexus time to start up on 'https://${SubjectWithoutCn}:8443'" 102 | Wait-Site Nexus 103 | 104 | # Build Credential Object, Connect to Nexus 105 | if ($Credential = Get-ChocoEnvironmentProperty NexusCredential) { 106 | Write-Verbose "Using stored Nexus Credential" 107 | } elseif (Test-Path 'C:\programdata\sonatype-work\nexus3\admin.password') { 108 | $securePw = (Get-Content 'C:\programdata\sonatype-work\nexus3\admin.password') | ConvertTo-SecureString -AsPlainText -Force 109 | $Credential = [System.Management.Automation.PSCredential]::new('admin', $securePw) 110 | } 111 | 112 | # Connect to Nexus 113 | Connect-NexusServer -Hostname $SubjectWithoutCn -Credential $Credential -UseSSL 114 | 115 | # Push ClientSetup.ps1 to raw repo 116 | $ClientScript = "$PSScriptRoot\scripts\ClientSetup.ps1" 117 | (Get-Content -Path $ClientScript) -replace "{{hostname}}", "$((Get-NexusLocalServiceUri) -replace '^https?:\/\/')" | Set-Content -Path ($TemporaryFile = New-TemporaryFile).FullName 118 | $null = New-NexusRawComponent -RepositoryName 'choco-install' -File $TemporaryFile.FullName -Name "ClientSetup.ps1" 119 | 120 | $NexusPw = Get-ChocoEnvironmentProperty ChocoUserPassword -AsPlainText 121 | 122 | # Update all sources with credentials and the new path 123 | foreach ($Repository in Get-NexusRepository -Format nuget | Where-Object Type -eq 'hosted') { 124 | $RepositoryUrl = "https://${SubjectWithoutCn}:8443/repository/$($Repository.Name)/index.json" 125 | 126 | $ChocoArgs = @( 127 | 'source', 128 | 'add', 129 | "--name='$($Repository.Name)'", 130 | "--source='$RepositoryUrl'", 131 | '--priority=1', 132 | "--user='chocouser'", 133 | "--password='$NexusPw'" 134 | ) 135 | & Invoke-Choco @ChocoArgs 136 | 137 | # Update Repository API key 138 | $chocoArgs = @('apikey', "--source='$RepositoryUrl'", "--api-key='$NuGetApiKey'") 139 | & Invoke-Choco @chocoArgs 140 | 141 | # Reset the NuGet v3 cache, such that it doesn't capture localhost as the FQDN 142 | Remove-NexusRepositoryFolder -RepositoryName $Repository.Name -Name v3 143 | } 144 | 145 | Update-Clixml -Properties @{ 146 | NexusUri = "https://$($SubjectWithoutCn):8443" 147 | NexusRepo = "https://${SubjectWithoutCn}:8443/repository/ChocolateyInternal/index.json" 148 | } 149 | 150 | <# Jenkins #> 151 | $JenkinsHome = "C:\ProgramData\Jenkins\.jenkins" 152 | $JenkinsPort = 7443 153 | 154 | Set-JenkinsLocationConfiguration -Url "https://$($SubjectWithoutCn):$($JenkinsPort)" -Path $JenkinsHome\jenkins.model.JenkinsLocationConfiguration.xml 155 | 156 | # Generate Jenkins keystore 157 | Set-JenkinsCertificate -Thumbprint $Certificate.Thumbprint -Port $JenkinsPort 158 | 159 | # Add firewall rule for Jenkins 160 | netsh advfirewall firewall add rule name="Jenkins-$($JenkinsPort)" dir=in action=allow protocol=tcp localport=$JenkinsPort 161 | 162 | # Update job parameters in Jenkins 163 | $NexusUri = Get-ChocoEnvironmentProperty NexusUri 164 | Update-JenkinsJobParameters -Replacement @{ 165 | "P_DST_URL" = "$NexusUri/repository/ChocolateyTest/index.json" 166 | "P_LOCAL_REPO_URL" = "$NexusUri/repository/ChocolateyTest/index.json" 167 | "P_TEST_REPO_URL" = "$NexusUri/repository/ChocolateyTest/index.json" 168 | "P_PROD_REPO_URL" = "$NexusUri/repository/ChocolateyInternal/index.json" 169 | } 170 | 171 | Update-Clixml -Properties @{ 172 | JenkinsUri = "https://$($SubjectWithoutCn):$($JenkinsPort)" 173 | } 174 | 175 | <# CCM #> 176 | # Update the service certificate 177 | Set-CcmCertificate -CertificateThumbprint $Certificate.Thumbprint 178 | 179 | # Remove old CCM web binding, and add new CCM web binding 180 | Stop-CcmService 181 | Remove-CcmBinding 182 | New-CcmBinding -Thumbprint $Certificate.Thumbprint 183 | Start-CcmService 184 | 185 | # Create the site hosting the certificate import script on port 80 186 | # Only run this if it's a self-signed cert which has 10-year validity 187 | if ($Certificate.NotAfter -gt (Get-Date).AddYears(5)) { 188 | $IsSelfSigned = $true 189 | .\scripts\New-IISCertificateHost.ps1 190 | } 191 | 192 | # Generate Register-C4bEndpoint.ps1 193 | $EndpointScript = "$PSScriptRoot\scripts\Register-C4bEndpoint.ps1" 194 | 195 | Invoke-TextReplacementInFile -Path $EndpointScript -Replacement @{ 196 | "{{ ClientSaltValue }}" = Get-ChocoEnvironmentProperty ClientSalt -AsPlainText 197 | "{{ ServiceSaltValue }}" = Get-ChocoEnvironmentProperty ServiceSalt -AsPlainText 198 | "{{ FQDN }}" = $SubjectWithoutCn 199 | 200 | # Set a default value for TrustCertificate if we're using a self-signed cert 201 | '(?\s+\$TrustCertificate)(?\s*=\s*\$true)?(?,)?(?!\))' = "`${Parameter}$( 202 | if (Test-SelfSignedCertificate -Certificate $Certificate) {' = $true'} 203 | )`${Comma}" 204 | } 205 | 206 | Update-Clixml -Properties @{ 207 | CCMWebPortal = "https://$($SubjectWithoutCn)/Account/Login" 208 | CCMServiceURL = "https://$($SubjectWithoutCn):24020/ChocolateyManagementService" 209 | CertSubject = $SubjectWithoutCn 210 | CertThumbprint = $Certificate.Thumbprint 211 | CertExpiry = $Certificate.NotAfter 212 | IsSelfSigned = $IsSelfSigned 213 | } 214 | } 215 | end { 216 | $ErrorActionPreference = $DefaultEap 217 | Stop-Transcript 218 | 219 | Complete-C4bSetup -SkipBrowserLaunch 220 | } -------------------------------------------------------------------------------- /Test-C4bSetup.ps1: -------------------------------------------------------------------------------- 1 | #requires -modules C4B-Environment 2 | [CmdletBinding()] 3 | Param( 4 | [Parameter()] 5 | [String] 6 | $Fqdn = $(Get-ChocoEnvironmentProperty CertSubject) 7 | ) 8 | process { 9 | if (-not (Get-Module Pester -ListAvailable).Where{$_.Version -gt "5.0"}) { 10 | Write-Host "Installing Pester 5 to run validation tests" 11 | $chocoArgs = @('install', 'pester', "--source='ChocolateyInternal'", '-y', '--no-progress', '--source="https://community.chocolatey.org/api/v2/"') 12 | & Invoke-Choco @chocoArgs 13 | } 14 | 15 | $files = (Get-ChildItem C:\choco-setup\files\tests\ -Recurse -Filter *.ps1).Fullname 16 | Write-Host "Configuring Pester to complete verification tests" 17 | $containers = $files | Foreach-Object { New-PesterContainer -Path $_ -Data @{ Fqdn = $Fqdn } } 18 | $configuration = [PesterConfiguration]@{ 19 | Run = @{ 20 | Container = $Containers 21 | Passthru = $true 22 | } 23 | Output = @{ 24 | Verbosity = 'Detailed' 25 | } 26 | TestResult = @{ 27 | Enabled = $true 28 | OutputFormat = 'NUnitXml' 29 | OutputPath = 'C:\choco-setup\test-results\verification.results.xml' 30 | } 31 | } 32 | 33 | $results = Invoke-Pester -Configuration $configuration 34 | if ($results.FailedCount -gt 0) { 35 | Compress-Archive -Path C:\choco-setup\test-results\verification.results.xml -DestinationPath "C:\choco-setup\files\support_archive.zip" 36 | Get-ChildItem C:\choco-setup\logs -Recurse -Include *.log,*.txt | Foreach-Object { Compress-Archive -Path $_.FullName -Update -DestinationPath "C:\choco-setup\files\support_archive.zip" } 37 | Write-Host "Logs have been collected into 'C:\choco-setup\files\support_archive.zip'." 38 | Write-Host "Please submit this archive to support@chocolatey.io so our team can assist you." 39 | } 40 | } -------------------------------------------------------------------------------- /c4b-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chocolatey/choco-quickstart-scripts/25068815484d6f1c44dde4de634802cd3ffc1599/c4b-server.png -------------------------------------------------------------------------------- /files/chocolatey.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | { "name": "chocolatey-agent" }, 4 | { "name": "chocolatey-compatibility.extension" }, 5 | { "name": "chocolatey-core.extension" }, 6 | { "name": "chocolatey-dotnetfx.extension" }, 7 | { "name": "chocolatey-licensed-jenkins-jobs" }, 8 | { "name": "chocolatey-licensed-jenkins-scripts" }, 9 | { "name": "chocolatey-management-database", "internalize": false }, 10 | { "name": "chocolatey-management-service", "internalize": false }, 11 | { "name": "chocolatey-management-web", "internalize": false }, 12 | { "name": "chocolatey-windowsupdate.extension" }, 13 | { "name": "chocolatey.extension" }, 14 | { "name": "chocolatey" }, 15 | { "name": "chocolateygui.extension" }, 16 | { "name": "chocolateygui" }, 17 | { "name": "dotnet-8.0-aspnetruntime", "version": "8.0.16" }, 18 | { "name": "dotnet-8.0-runtime", "version": "8.0.16" }, 19 | { "name": "dotnet-aspnetcoremodule-v2", "version": "18.0.25074" }, 20 | { "name": "dotnetfx" }, 21 | { "name": "jenkins" }, 22 | { "name": "KB2919355", "internalize": false }, 23 | { "name": "KB2919442", "internalize": false }, 24 | { "name": "KB2999226", "internalize": false }, 25 | { "name": "KB3033929", "internalize": false }, 26 | { "name": "KB3035131", "internalize": false }, 27 | { "name": "microsoft-edge" }, 28 | { "name": "nexushell", "version": "1.2.0" }, 29 | { "name": "nexus-repository" }, 30 | { "name": "pester", "internalize": false }, 31 | { "name": "sql-server-express" }, 32 | { "name": "temurin21jre" }, 33 | { "name": "vcredist140" } 34 | ] 35 | } -------------------------------------------------------------------------------- /files/jenkins.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | { "name": "apache-httpcomponents-client-4-api", "version": "4.5.14-269.vfa_2321039a_83" }, 4 | { "name": "asm-api", "version": "9.8-135.vb_2239d08ee90" }, 5 | { "name": "bouncycastle-api", "version": "2.30.1.80-256.vf98926042a_9b_" }, 6 | { "name": "branch-api", "version": "2.1217.v43d8b_b_d8b_2c7" }, 7 | { "name": "caffeine-api", "version": "3.2.0-166.v72a_6d74b_870f" }, 8 | { "name": "cloudbees-folder", "version": "6.1012.v79a_86a_1ea_c1f" }, 9 | { "name": "display-url-api", "version": "2.209.v582ed814ff2f" }, 10 | { "name": "durable-task", "version": "587.v84b_877235b_45" }, 11 | { "name": "instance-identity", "version": "201.vd2a_b_5a_468a_a_6" }, 12 | { "name": "ionicons-api", "version": "82.v0597178874e1" }, 13 | { "name": "jakarta-activation-api", "version": "2.1.3-2" }, 14 | { "name": "jakarta-mail-api", "version": "2.1.3-2" }, 15 | { "name": "javax-activation-api", "version": "1.2.0-8" }, 16 | { "name": "javax-mail-api", "version": "1.6.2-11" }, 17 | { "name": "mailer", "version": "489.vd4b_25144138f" }, 18 | { "name": "pipeline-groovy-lib", "version": "752.vdddedf804e72" }, 19 | { "name": "scm-api", "version": "704.v3ce5c542825a_" }, 20 | { "name": "script-security", "version": "1373.vb_b_4a_a_c26fa_00" }, 21 | { "name": "structs", "version": "343.vdcf37b_a_c81d5" }, 22 | { "name": "variant", "version": "70.va_d9f17f859e0" }, 23 | { "name": "workflow-api", "version": "1371.ve334280b_d611" }, 24 | { "name": "workflow-basic-steps", "version": "1079.vce64b_a_929c5a_" }, 25 | { "name": "workflow-cps", "version": "4046.v90b_1b_9edec67" }, 26 | { "name": "workflow-durable-task-step", "version": "1405.v1fcd4a_d00096" }, 27 | { "name": "workflow-job", "version": "1520.v56d65e3b_4566" }, 28 | { "name": "workflow-multibranch", "version": "806.vb_b_688f609ee9" }, 29 | { "name": "workflow-scm-step", "version": "437.v05a_f66b_e5ef8" }, 30 | { "name": "workflow-step-api", "version": "700.v6e45cb_a_5a_a_21" }, 31 | { "name": "workflow-support", "version": "968.v8f17397e87b_8" } 32 | ] 33 | } -------------------------------------------------------------------------------- /modules/C4B-Environment/C4B-Environment.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'c4b-helpers' 3 | # 4 | # Generated by: james 5 | # 6 | # Generated on: 18/09/2024 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'C4B-Environment.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '0.0.1' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '29a70562-5048-4ab9-9654-1bd5e962b1c5' 22 | 23 | # Author of this module 24 | Author = 'james' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Chocolatey Software' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) james. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | # Description = '' 34 | 35 | # Minimum version of the PowerShell engine required by this module 36 | # PowerShellVersion = '' 37 | 38 | # Name of the PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # ClrVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = '*' 73 | 74 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 75 | CmdletsToExport = '*' 76 | 77 | # Variables to export from this module 78 | VariablesToExport = '*' 79 | 80 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 81 | AliasesToExport = '*' 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | # FileList = @() 91 | 92 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | # Tags applied to this module. These help with module discovery in online galleries. 98 | # Tags = @() 99 | 100 | # A URL to the license for this module. 101 | # LicenseUri = '' 102 | 103 | # A URL to the main website for this project. 104 | # ProjectUri = '' 105 | 106 | # A URL to an icon representing this module. 107 | # IconUri = '' 108 | 109 | # ReleaseNotes of this module 110 | # ReleaseNotes = '' 111 | 112 | # Prerelease string of this module 113 | # Prerelease = '' 114 | 115 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 116 | # RequireLicenseAcceptance = $false 117 | 118 | # External dependent modules of this module 119 | ExternalModuleDependencies = @( 120 | "NexuShell" 121 | ) 122 | 123 | } # End of PSData hashtable 124 | 125 | } # End of PrivateData hashtable 126 | 127 | # HelpInfo URI of this module 128 | # HelpInfoURI = '' 129 | 130 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 131 | # DefaultCommandPrefix = '' 132 | 133 | } 134 | 135 | -------------------------------------------------------------------------------- /modules/C4B-Environment/ReadmeTemplate.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chocolatey For Business Quickstart Environment 5 | 126 | 137 | 138 |
139 |
140 |
141 | Chocolatey Logo 142 |
143 |
144 |

Chocolatey for Business Quickstart Environment

145 |

Your C4B environment setup has completed!

146 |
147 |
148 |
149 |
150 |

Getting Started

151 | 158 |

Accounts and Secrets

159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 |
NameUrlUsernamePassword
Chocolatey Central Management{{ ccm_fqdn }}:{{ ccm_port }}ccmadmin
{{ ccm_password | e }}
Nexus{{ nexus_fqdn }}:{{ nexus_port }}admin
{{ nexus_password | e }}
{{ nexus_client_username }}
{{ nexus_client_password | e }}
Jenkins{{ jenkins_fqdn }}:{{ jenkins_port }}admin
{{ jenkins_password | e }}
187 |

188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 |
NameValue
Chocolatey Central Management Encryption Passphrase
{{ ccm_encryption_password | e }}
Chocolatey Central Management Client Salt
{{ ccm_client_salt | e }}
Chocolatey Central Management Service Salt
{{ ccm_service_salt | e }}
Chocolatey Package Uploader API Key
{{ lookup('file', 'credentials/nexus_apikey') | default('Unavailable') }}
208 |
209 |

📝 Note

210 |
211 |

The tables above provides credentials to login to each of the services made available as part of the deployment.

212 |

Ensure you change them, store your new credentials and secrets in a secure manner, and delete the stored files.

213 |

214 | 215 |

Adding Computers to Chocolatey Central Management

216 |

To add a computer to Chocolatey Central Management you must register it as a Chocolatey For Business (C4B) Endpoint

217 |

To register a computer please run the Register-C4bEndpoint.ps1 script as shown in Step 7: Setting up Endpoints of the Quickstart Guide.

218 |
219 | 220 | -------------------------------------------------------------------------------- /scripts/ClientSetup.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Completes client setup for a client machine to communicate with CCM. 4 | #> 5 | [CmdletBinding(DefaultParameterSetName = 'Default')] 6 | param( 7 | # The URL of the the internal Nexus repository to install Chocolatey from. 8 | # This URL will be used to create the internal package source configuration. 9 | [Parameter()] 10 | [Alias('Url')] 11 | [string] 12 | $RepositoryUrl = 'https://{{hostname}}/repository/ChocolateyInternal/index.json', 13 | 14 | # The credential used to access the internal Nexus repository. 15 | [Parameter(Mandatory)] 16 | [Alias('Credential')] 17 | [pscredential] 18 | $RepositoryCredential, 19 | 20 | # Specifies a target version of Chocolatey to install. By default, the 21 | # latest stable version is installed. 22 | [Parameter()] 23 | [string] 24 | $ChocolateyVersion = $env:chocolateyVersion, 25 | 26 | # Specifies whether to ignore any configured proxy. This will override any 27 | # specified proxy environment variables. 28 | [Parameter()] 29 | [switch] 30 | $IgnoreProxy = [bool]$env:chocolateyIgnoreProxy, 31 | 32 | # The URL of a proxy server to use for connecting to the repository. 33 | [Parameter(Mandatory = $true, ParameterSetName = 'Proxy')] 34 | $ProxyUrl = $env:chocolateyProxyLocation, 35 | 36 | # The credentials, if required, to connect to the proxy server. 37 | [Parameter(ParameterSetName = 'Proxy')] 38 | [pscredential] 39 | $ProxyCredential, 40 | 41 | # Client salt value used to populate the centralManagementClientCommunicationSaltAdditivePassword 42 | # value in the Chocolatey config file 43 | [Parameter()] 44 | [Alias('ClientSalt')] 45 | [string] 46 | $ClientCommunicationSalt, 47 | 48 | # Server salt value used to populate the centralManagementServiceCommunicationSaltAdditivePassword 49 | # value in the Chocolatey config file 50 | [Parameter()] 51 | [Alias('ServerSalt')] 52 | [string] 53 | $ServiceCommunicationSalt, 54 | 55 | # Install the Chocolatey Licensed Extension with right-click context menus available 56 | [Parameter()] 57 | [Switch] 58 | $IncludePackageTools, 59 | 60 | # Allows for the application of user-defined configuration that is applied after the base configuration. 61 | # Can override base configuration with this parameter 62 | [Parameter()] 63 | [Hashtable] 64 | $AdditionalConfiguration, 65 | 66 | # Allows for the toggling of additional features that is applied after the base configuration. 67 | # Can override base configuration with this parameter 68 | [Parameter()] 69 | [Hashtable] 70 | $AdditionalFeatures, 71 | 72 | # Allows for the installation of additional packages after the system base packages have been installed. 73 | [Parameter()] 74 | [Hashtable[]] 75 | $AdditionalPackages, 76 | 77 | # Allows for the addition of alternative sources after the base conifguration has been applied. 78 | # Can override base configuration with this parameter 79 | [Parameter()] 80 | [Hashtable[]] 81 | $AdditionalSources 82 | ) 83 | 84 | Set-ExecutionPolicy Bypass -Scope Process -Force 85 | 86 | $hostName = ([uri]$RepositoryUrl).DnsSafeHost 87 | 88 | $params = @{ 89 | ChocolateyVersion = $ChocolateyVersion 90 | IgnoreProxy = $IgnoreProxy 91 | UseNativeUnzip = $true 92 | } 93 | 94 | if (-not $IgnoreProxy -and $ProxyUrl) { 95 | $Proxy = [System.Net.WebProxy]::new( 96 | $ProxyUrl, 97 | $true # Bypass Local Addresses 98 | ) 99 | $params.Add('ProxyUrl', $ProxyUrl) 100 | 101 | if ($ProxyCredential) { 102 | $Proxy.Credentials = $ProxyCredential 103 | $params.Add('ProxyCredential', $ProxyCredential) 104 | } elseif ($DefaultProxyCredential = [System.Net.CredentialCache]::DefaultCredentials) { 105 | $Proxy.Credentials = $DefaultProxyCredential 106 | $params.Add('ProxyCredential', $DefaultProxyCredential) 107 | } 108 | } 109 | 110 | $webClient = New-Object System.Net.WebClient 111 | if ($RepositoryCredential) { 112 | $webClient.Credentials = $RepositoryCredential.GetNetworkCredential() 113 | } 114 | 115 | # Find the latest version of Chocolatey, if a version was not specified 116 | $NupkgUrl = if (-not $ChocolateyVersion) { 117 | $QueryUrl = (($RepositoryUrl -replace '/index\.json$'), "v3/registration/Chocolatey/index.json") -join '/' 118 | $Result = $webClient.DownloadString($QueryUrl) | ConvertFrom-Json 119 | $Result.items.items[-1].packageContent 120 | } else { 121 | # Otherwise, assume the URL 122 | "$($RepositoryUrl -replace '/index\.json$')/v3/content/chocolatey/$($ChocolateyVersion)/chocolatey.$($ChocolateyVersion).nupkg" 123 | } 124 | 125 | $webClient.Proxy = if ($Proxy -and -not $Proxy.IsBypassed($NupkgUrl)) {$Proxy} 126 | 127 | # Download the NUPKG 128 | $NupkgPath = Join-Path $env:TEMP "$(New-Guid).zip" 129 | $webClient.DownloadFile($NupkgUrl, $NupkgPath) 130 | 131 | # Add Parameter for ChocolateyDownloadUrl, that is the NUPKG path 132 | $params.Add('ChocolateyDownloadUrl', $NupkgPath) 133 | $InstallScriptUrl = $RepositoryUrl -replace '\/repository\/(?.+)\/(index.json)?$', '/repository/choco-install/ChocolateyInstall.ps1' 134 | $webClient.Proxy = if ($Proxy -and -not $Proxy.IsBypassed($InstallScriptUrl)) {$Proxy} 135 | $script = $webClient.DownloadString($InstallScriptUrl) 136 | & ([scriptblock]::Create($script)) @params 137 | 138 | # If FIPS is enabled, configure Chocolatey to use FIPS compliant checksums 139 | $fipsStatus = Get-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Lsa\FipsAlgorithmPolicy" -Name Enabled 140 | if ($fipsStatus.Enabled -eq 1) { 141 | Write-Warning -Message "FIPS is enabled on this system. Ensuring Chocolatey uses FIPS compliant checksums" 142 | choco feature enable --name='useFipsCompliantChecksums' 143 | } 144 | 145 | choco config set cacheLocation $env:ChocolateyInstall\choco-cache 146 | choco config set commandExecutionTimeoutSeconds 14400 147 | 148 | # Nexus NuGet V3 Compatibility 149 | choco feature disable --name="'usePackageRepositoryOptimizations'" 150 | 151 | choco source add --name="'ChocolateyInternal'" --source="'$RepositoryUrl'" --allow-self-service --user="'$($RepositoryCredential.UserName)'" --password="'$($RepositoryCredential.GetNetworkCredential().Password)'" --priority=1 152 | 153 | choco source disable --name="'Chocolatey'" 154 | choco source disable --name="'chocolatey.licensed'" 155 | 156 | choco upgrade chocolatey-license --confirm --source="'ChocolateyInternal'" 157 | choco upgrade chocolatey.extension --confirm --source="'ChocolateyInternal'" --no-progress @( 158 | if (-not $IncludePackageTools) { 159 | '--params="/NoContextMenu"' 160 | } else { 161 | Write-Verbose "IncludePackageTools was passed. Right-Click context menus will be available for installers, .nupkg, and .nuspec file types!" 162 | } 163 | ) 164 | 165 | choco upgrade chocolateygui --confirm --source="'ChocolateyInternal'" --no-progress 166 | choco upgrade chocolateygui.extension --confirm --source="'ChocolateyInternal'" --no-progress 167 | 168 | choco upgrade chocolatey-agent --confirm --source="'ChocolateyInternal'" 169 | 170 | # Chocolatey Package Upgrade Resilience 171 | choco feature enable --name="'excludeChocolateyPackagesDuringUpgradeAll'" 172 | 173 | # Self-Service configuration 174 | choco feature disable --name="'showNonElevatedWarnings'" 175 | choco feature enable --name="'useBackgroundService'" 176 | choco feature enable --name="'useBackgroundServiceWithNonAdministratorsOnly'" 177 | choco feature enable --name="'allowBackgroundServiceUninstallsFromUserInstallsOnly'" 178 | choco config set --name="'backgroundServiceAllowedCommands'" --value="'install,upgrade,uninstall'" 179 | 180 | # Enable Package Hash Validation (Good security practice) 181 | choco feature enable --name="'usePackageHashValidation'" 182 | 183 | # CCM Check-in Configuration 184 | choco config set CentralManagementServiceUrl "https://${hostName}:24020/ChocolateyManagementService" 185 | if ($ClientCommunicationSalt) { 186 | choco config set centralManagementClientCommunicationSaltAdditivePassword $ClientCommunicationSalt 187 | } 188 | if ($ServiceCommunicationSalt) { 189 | choco config set centralManagementServiceCommunicationSaltAdditivePassword $ServiceCommunicationSalt 190 | } 191 | choco feature enable --name="'useChocolateyCentralManagement'" 192 | choco feature enable --name="'useChocolateyCentralManagementDeployments'" 193 | 194 | if ($AdditionalConfiguration -or $AdditionalFeatures -or $AdditionalSources -or $AdditionalPackages) { 195 | Write-Host "Applying user supplied configuration" 196 | } 197 | 198 | if ($AdditionalConfiguration) { 199 | <# 200 | We expect to pass in a hashtable with configuration information with the following shape: 201 | 202 | @{ 203 | BackgroundServiceAllowedCommands = 'install,upgrade,uninstall' 204 | commandExecutionTimeoutSeconds = 6000 205 | } 206 | #> 207 | 208 | $AdditionalConfiguration.GetEnumerator() | ForEach-Object { 209 | $Config = [System.Collections.Generic.list[string]]::new() 210 | $Config.Add('config') 211 | $Config.Add('set') 212 | $Config.Add("--name='$($_.Key)'") 213 | $Config.Add("--value='$($_.Value)'") 214 | 215 | & choco @Config 216 | } 217 | } 218 | 219 | if ($AdditionalFeatures) { 220 | <# 221 | We expect to pass in feature information as a hashtable with the following shape: 222 | 223 | @{ 224 | useBackgroundservice = 'Enabled' 225 | } 226 | #> 227 | $AdditionalFeatures.GetEnumerator() | ForEach-Object { 228 | $Feature = [System.Collections.Generic.list[string]]::new() 229 | $Feature.Add('feature') 230 | 231 | $state = switch ($_.Value) { 232 | 'Enabled' { 'enable' } 233 | 'Disabled' { 'disable' } 234 | default { Write-Error 'State must be either Enabled or Disabled' } 235 | } 236 | 237 | $Feature.Add($state) 238 | $Feature.add("--name='$($_.Key)'") 239 | & choco @Feature 240 | } 241 | } 242 | 243 | if ($AdditionalSources) { 244 | <# 245 | We expect a user to pass in a hashtable with source information with the following shape: 246 | @{ 247 | Name = 'MySource' 248 | Source = 'https://nexus.fabrikam.com/repository/MyChocolateySource' 249 | # Optional items 250 | Credentials = $MySourceCredential 251 | AllowSelfService = $true 252 | AdminOnly = $true 253 | BypassProxy = $true 254 | Priority = 10 255 | Certificate = 'C:\cert.pfx' 256 | CertificatePassword = 's0mepa$$' 257 | } 258 | #> 259 | foreach ($Source in $AdditionalSources) { 260 | $SourceSplat = [System.Collections.Generic.List[string]]::new() 261 | # Required items 262 | $SourceSplat.Add('source') 263 | $SourceSplat.Add('add') 264 | $SourceSplat.Add("--name='$($Source.Name)'") 265 | $SourceSplat.Add("--source='$($Source.Source)'") 266 | 267 | # Add credentials if source has them 268 | if ($Source.ContainsKey('Credentials')) { 269 | $SourceSplat.Add("--user='$($Source.Credentials.Username)'") 270 | $SourceSplat.Add("--password='$($Source.Credentials.GetNetworkCredential().Password)'") 271 | } 272 | 273 | switch ($true) { 274 | $Source['AllowSelfService'] { $SourceSplat.add('--allow-self-service') } 275 | $Source['AdminOnly'] { $SourceSplat.Add('--admin-only') } 276 | $Source['BypassProxy'] { $SourceSplat.Add('--bypass-proxy') } 277 | $Source.ContainsKey('Priority') { $SourceSplat.Add("--priority='$($Source.Priority)'") } 278 | $Source.ContainsKey('Certificate') { $SourceSplat.Add("--cert='$($Source.Certificate)'") } 279 | $Source.ContainsKey('CerfificatePassword') { $SourceSplat.Add("--certpassword='$($Source.CertificatePassword)'") } 280 | } 281 | } 282 | 283 | & choco @SourceSplat 284 | } 285 | 286 | if ($AdditionalPackages) { 287 | <# 288 | We expect to pass in a hashtable with package information with the following shape: 289 | 290 | @{ 291 | Id = 'firefox' 292 | #Optional 293 | Version = 123.4.56 294 | Pin = $true 295 | } 296 | #> 297 | foreach ($package in $AdditionalPackages.GetEnumerator()) { 298 | $PackageSplat = [System.Collections.Generic.list[string]]::new() 299 | $PackageSplat.add('install') 300 | $PackageSplat.add($package['Id']) 301 | 302 | switch ($true) { 303 | $package.ContainsKey('Version') { $PackageSplat.Add("--version='$($package.version)'") } 304 | $package.ContainsKey('Pin') { $PackageSplat.Add('--pin') } 305 | } 306 | 307 | # Ensure packages install and they don't flood the console output 308 | $PackageSplat.Add('-y') 309 | $PackageSplat.Add('--no-progress') 310 | 311 | & choco @PackageSplat 312 | } 313 | } -------------------------------------------------------------------------------- /scripts/Create-ChocoLicensePkg.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Creates a Chocolatey package which can be used to deploy the license XML file to 4 | client machines. 5 | 6 | .DESCRIPTION 7 | Finds the license file at the specified location (in your Chocolatey install 8 | folder by default) and creates a simple package around it. The resulting package 9 | is pushed to the ChocolateyInternal Nexus repository by default. 10 | #> 11 | [CmdletBinding()] 12 | param( 13 | # Local path used to build the license package. 14 | [Parameter()] 15 | [string] 16 | $PackagesPath = "$env:SystemDrive\choco-setup\files\files", 17 | 18 | # Path to the license file. 19 | [Parameter()] 20 | [string] 21 | $LicensePath = "$env:ChocolateyInstall\license\chocolatey.license.xml", 22 | 23 | # The package ID of the generated license package. 24 | [Parameter()] 25 | [string] 26 | $LicensePackageId = "chocolatey-license", 27 | 28 | # The version of the license package to push. If not set, the license 29 | # package version is automatically generated from the expiry date in the 30 | # license file. 31 | [Parameter()] 32 | [string] 33 | $LicensePackageVersion 34 | ) 35 | 36 | if (-not (Test-Path $LicensePath)) { 37 | if ($PSBoundParameters.ContainsKey('LicensePath')) { 38 | throw "License file '$LicensePath' not found. Please supply a path to an existing license file." 39 | } 40 | 41 | throw "License file not found at '$LicensePath'. Please add license to this location before running this script, or supply a -LicensePath value manually." 42 | } 43 | 44 | $PackagingFolder = "$env:SystemDrive\choco-setup\packaging" 45 | $licensePackageFolder = "$PackagingFolder\$LicensePackageId" 46 | $licensePackageNuspec = "$licensePackageFolder\$LicensePackageId.nuspec" 47 | 48 | # Get license expiration date and node count 49 | [xml]$licenseXml = Get-Content -Path $LicensePath 50 | $licenseExpiration = [datetimeoffset]::Parse("$($licenseXml.SelectSingleNode('/license').expiration) +0") 51 | $null = $licenseXml.license.name -match "(?<=\[).*(?=\])" 52 | $licenseNodeCount = $Matches.Values -replace '\s[A-Za-z]+','' 53 | 54 | if ($licenseExpiration -lt [datetimeoffset]::UtcNow) { 55 | Write-Warning "THE LICENSE FILE AT '$LicensePath' is EXPIRED. This is the file used by this script to generate this package, not at '$licensePackageFolder'" 56 | Write-Warning "Please update the license file correctly in the environment FIRST, then rerun this script." 57 | throw "License is expired as of $($licenseExpiration.ToString()). Please use an up to date license." 58 | } 59 | 60 | if (-not $LicensePackageVersion) { 61 | $LicensePackageVersion = ($licenseExpiration | Get-Date -Format 'yyyy.MM.dd') + '.' + "$licenseNodeCount" 62 | } 63 | 64 | # Ensure the packaging folder exists 65 | Write-Verbose "Generating package/packaging folders at '$PackagingFolder'" 66 | New-Item $PackagingFolder -ItemType Directory -Force | Out-Null 67 | New-Item $PackagesPath -ItemType Directory -Force | Out-Null 68 | 69 | # Create a new package 70 | Write-Verbose "Creating package named '$LicensePackageId'" 71 | New-Item $licensePackageFolder -ItemType Directory -Force | Out-Null 72 | New-Item "$licensePackageFolder\tools" -ItemType Directory -Force | Out-Null 73 | 74 | # Set the installation script 75 | Write-Verbose "Setting install and uninstall scripts..." 76 | @' 77 | $ErrorActionPreference = 'Stop' 78 | $toolsDir = Split-Path -Parent $MyInvocation.MyCommand.Definition 79 | $licenseFile = "$toolsDir\chocolatey.license.xml" 80 | 81 | New-Item "$env:ChocolateyInstall\license" -ItemType Directory -Force 82 | Copy-Item -Path $licenseFile -Destination $env:ChocolateyInstall\license\chocolatey.license.xml -Force 83 | Write-Output "The license has been installed." 84 | '@ | Set-Content -Path "$licensePackageFolder\tools\chocolateyInstall.ps1" -Encoding UTF8 -Force 85 | 86 | # Set the uninstall script 87 | @' 88 | Remove-Item -Path "$env:ChocolateyInstall\license\chocolatey.license.xml" -Force 89 | Write-Output "The license has been removed." 90 | '@ | Set-Content -Path "$licensePackageFolder\tools\chocolateyUninstall.ps1" -Encoding UTF8 -Force 91 | 92 | # Copy the license to the package directory 93 | Write-Verbose "Copying license to package from '$LicensePath' to package location." 94 | Copy-Item -Path $LicensePath -Destination "$licensePackageFolder\tools\chocolatey.license.xml" -Force 95 | 96 | # Set the nuspec 97 | Write-Verbose "Setting nuspec..." 98 | @" 99 | 100 | 101 | 102 | chocolatey-license 103 | $LicensePackageVersion 104 | 105 | Chocolatey License 106 | Chocolatey Software, Inc 107 | chocolatey license 108 | Installs the Chocolatey commercial license file. 109 | This package ensures installation of the Chocolatey commercial license file. 110 | 111 | This should be installed internally prior to installing other packages, directly after Chocolatey is installed and prior to installing `chocolatey.extension` and `chocolatey-agent`. 112 | 113 | The order for scripting is this: 114 | * chocolatey 115 | * chocolatey-license 116 | * chocolatey.extension 117 | * chocolatey-agent 118 | 119 | If items are installed in any other order, it could have strange effects or fail. 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | "@.Trim() | Set-Content -Path "$licensePackageNuspec" -Encoding UTF8 -Force 128 | 129 | # Package up everything 130 | Write-Verbose "Creating license package..." 131 | Invoke-Choco pack $licensePackageNuspec --output-directory="$PackagesPath" 132 | Write-Verbose "Package has been created and is ready at $PackagesPath" 133 | 134 | Write-Verbose "Installing newly created package on this machine, making updates to license easier in the future, if pushed from another location later." 135 | Invoke-Choco upgrade chocolatey-license -y --source="'$PackagesPath'" 136 | -------------------------------------------------------------------------------- /scripts/Get-Helpers.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot\..\modules\c4b-environment -------------------------------------------------------------------------------- /scripts/Import-ChocoServerCertificate.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Connects to the QDE server and adds its SSL certificate to the local certificate 4 | store. 5 | #> 6 | [CmdletBinding()] 7 | param( 8 | # The hostname of the QDE server to retrieve the SSL certificate from. 9 | [Parameter()] 10 | [Alias('NexusServer', 'NexusUrl')] 11 | [string] 12 | $ComputerName = '{{hostname}}' 13 | ) 14 | 15 | add-type -TypeDefinition @" 16 | using System; 17 | using System.Net; 18 | using System.Net.Security; 19 | using System.Security.Cryptography.X509Certificates; 20 | 21 | public static class Dummy { 22 | public static bool ReturnTrue(object sender, 23 | X509Certificate certificate, 24 | X509Chain chain, 25 | SslPolicyErrors sslPolicyErrors) { return true; } 26 | 27 | public static RemoteCertificateValidationCallback GetDelegate() { 28 | return new RemoteCertificateValidationCallback(Dummy.ReturnTrue); 29 | } 30 | } 31 | "@ 32 | 33 | $callback = [System.Net.ServicePointManager]::ServerCertificateValidationCallback = [dummy]::GetDelegate() 34 | 35 | #Do it Matthias's way 36 | function Get-RemoteCertificate { 37 | param( 38 | [Alias('CN')] 39 | [Parameter(Mandatory = $true, Position = 0)] 40 | [string]$ComputerName, 41 | 42 | [Parameter(Position = 1)] 43 | [UInt16]$Port = 8443 44 | ) 45 | 46 | $tcpClient = New-Object System.Net.Sockets.TcpClient($ComputerName, $Port) 47 | $sslProtocolType = [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 48 | try { 49 | $tlsClient = New-Object System.Net.Security.SslStream($tcpClient.GetStream(),'false',$callback) 50 | $tlsClient.AuthenticateAsClient($ComputerName,$null,$sslProtocolType,$false) 51 | 52 | return $tlsClient.RemoteCertificate -as [System.Security.Cryptography.X509Certificates.X509Certificate2] 53 | } 54 | finally { 55 | if ($tlsClient -is [IDisposable]) { 56 | $tlsClient.Dispose() 57 | } 58 | 59 | $tcpClient.Dispose() 60 | } 61 | } 62 | 63 | #Create request to CCM Service URL to get Certificate 64 | Write-Output "Connecting to '$ComputerName' to get certificate information" 65 | $certificateToAdd = Get-RemoteCertificate -ComputerName $ComputerName 66 | 67 | #Create CertStore object where the certificate needs "put" 68 | Write-Host "Adding Cert to LocalMachine\TrustedPeople Store" 69 | $Store = [System.Security.Cryptography.X509Certificates.StoreName]::TrustedPeople 70 | $Location = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine 71 | $certStore = New-Object System.Security.Cryptography.X509Certificates.X509Store($Store, $Location) 72 | 73 | try { 74 | $certStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) 75 | $certStore.Add($certificateToAdd) 76 | } 77 | finally { 78 | $certStore.Close() 79 | Remove-Variable certstore 80 | } 81 | -------------------------------------------------------------------------------- /scripts/New-IISCertificateHost.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Creates the `ChocolateyInstall` IIS fileshare site. 4 | 5 | .DESCRIPTION 6 | Creates a new IIS website named `ChocolateyInstall` which hosts the 7 | Import-ChocoServerCertificate.ps1 for clients to retrieve and run during their 8 | setup. 9 | 10 | If you have a need to re-create this for any reason, ensure the existing 11 | `ChocolateyInstall` IIS site has been disabled and removed. 12 | #> 13 | [CmdletBinding()] 14 | param( 15 | # The path to a local directory which will be used to host the 16 | # Import-ChocoServerCertificate.ps1 file over IIS for clients to utilize. 17 | [Parameter()] 18 | [Alias('LocalDir')] 19 | [string] 20 | $Path = 'C:\tools\ChocolateyInstall' 21 | ) 22 | 23 | Import-Module WebAdministration 24 | 25 | $hostName = [System.Net.Dns]::GetHostName() 26 | $domainName = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().DomainName 27 | 28 | if (-not $hostName.endswith($domainName)) { 29 | $hostName += "." + $domainName 30 | } 31 | 32 | if (-not (Test-Path $Path)) { 33 | $null = New-Item -Path $Path -ItemType Directory -Force 34 | } 35 | 36 | $ImportScript = Join-Path $Path "Import-ChocoServerCertificate.ps1" 37 | if (-not (Test-Path $ImportScript)) { 38 | Copy-Item -Path "$PSScriptRoot/Import-ChocoServerCertificate.ps1" -Destination $Path 39 | } 40 | (Get-Content -Path $ImportScript) -replace "{{hostname}}", $HostName | Set-Content -Path $ImportScript 41 | 42 | 43 | $siteName = 'C4bSslCertificateImport' 44 | if (-not (Get-Website -Name $siteName)) { 45 | Write-Host "Creating Website: $siteName" -ForegroundColor Green 46 | $null = New-Website -Name $siteName -Port 80 -PhysicalPath $Path -Force 47 | Add-WebConfigurationProperty -PSPath IIS: -Filter system.webServer/staticContent -Name "." -Value @{ fileExtension = '.ps1'; mimeType = 'text/plain' } 48 | } else { 49 | Write-Host "Website for hosting certificate import already created" -ForegroundColor Green 50 | } 51 | 52 | Write-Host "Restarting IIS to refresh bindings" -ForegroundColor Green 53 | $null = iisreset 54 | 55 | if ((Get-Website -Name $siteName).State -ne 'Started') { 56 | Start-Website -Name $siteName 57 | } 58 | 59 | if ((Get-Website -Name 'Default Web Site')) { 60 | Get-Website -Name 'Default Web Site' | Remove-Website 61 | } else { 62 | Write-Host "Default website already removed" -ForegroundColor Green 63 | } 64 | 65 | Write-Host "IIS website started on port 80 hosting Import-ChocoServerCertificate.ps1 from $Path" 66 | -------------------------------------------------------------------------------- /scripts/Register-C4bEndpoint.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Deploys Chocolatey onto an endpoint. 4 | 5 | .EXAMPLE 6 | 7 | Some endpoints may require a different set of features. The default installation will apply our _recommended_ configuration. 8 | However, you can override these defaults or enable/disable additional features by providing the `-AdditionalFeatures` parameter. 9 | 10 | In this example we will disable the use of the background service so non-admin users cannot use Chocolatey (not recommended), and enable Gloabl Confirmation so you no longer need to pass -y when performing a package operation. 11 | 12 | . .\Register-C4bEndpoint.ps1 -RepositoryCredential (Get-Credential) -AdditionalFeatures @{ useBackgroundService = 'Disabled'; allowGlobalCOnfirmation = 'Enabled' } 13 | 14 | .EXAMPLE 15 | You can apply custom configuration which overrides the defaults or provides additional configuration by providing the `-AdditionalConfiguration` parameter. 16 | The following example sets the `centralManagementReportPackagesTimerIntervalInSeconds` configuration item to 21600 seconds (6 hours). 17 | 18 | . .\Register-C4bEndpoint.ps1 -RepositoryCredential (Get-Credential) -AdditionalConfiguration @{ 'centralManagementReportPackagesTimerIntervalInSeconds' = '21600'} 19 | 20 | .EXAMPLE 21 | You can include additional Chocolatey sources during the installation process by providing the `-AdditionalSources` parameter. 22 | 23 | . .\Register-C4bEndpoint.ps1 -RepositoryCredential (Get-Credential) -AdditionalSources @{Name = 'ChocolateyUpstream'; Source = 'https://community.chocolatey.org/api/v2/'} 24 | 25 | .EXAMPLE 26 | This example include Packaging Tools and sets up a local folder source for package development testing. 27 | The local folder must exist prior to using this source. 28 | 29 | . .\Register-C4bEndpoint.ps1 -RepositoryCredential (Get-Credential) -AdditionalSources @{Name = 'LocalTest'; Source = 'C:\packages\testing'} 30 | 31 | 32 | .EXAMPLE 33 | The following example installs the notepadplusplus.install package. 34 | 35 | . .\Register-C4bEndpoint.ps1 -RepositoryCredential (Get-Credential) -AdditionalPackages @{Id ='notepadplusplus.install'} 36 | 37 | .EXAMPLE 38 | The following example installs version 8.7.5 of the notepadplusplus.install package. 39 | 40 | . .\Register-C4bEndpoint.ps1 -RepositoryCredential (Get-Credential) -AdditionalPackages @{Id ='notepadplusplus.install'; Version = '8.7.5'} 41 | 42 | .EXAMPLE 43 | The following example installs version 8.7.5 of the notepadplusplus.install package and pins it so that it is not upgraded when using `choco upgrade` 44 | To upgrade this package, you will need to first unpin it, and then perform the upgrade. 45 | 46 | . .\Register-C4bEndpoint.ps1 -RepositoryCredential (Get-Credential) -AdditionalPackages @{Id ='notepadplusplus.install'; Version = '8.7.5'; Pin = $true} 47 | 48 | .NOTES 49 | 50 | Full documentation is available at https://docs.chocolatey.org/en-us/c4b-environments/quick-start-environment/advanced-client-configuration/ 51 | #> 52 | [CmdletBinding(HelpUri = 'https://docs.chocolatey.org/en-us/c4b-environments/quick-start-environment/advanced-client-configuration/')] 53 | Param( 54 | # The DNS name of the server that hosts your repository, Jenkins, and Chocolatey Central Management 55 | [Parameter()] 56 | [String] 57 | $Fqdn = '{{ FQDN }}', 58 | 59 | # Client salt value used to populate the centralManagementClientCommunicationSaltAdditivePassword 60 | # value in the Chocolatey config file 61 | [Parameter()] 62 | [String] 63 | $ClientCommunicationSalt = '{{ ClientSaltValue }}', 64 | 65 | # Server salt value used to populate the centralManagementServiceCommunicationSaltAdditivePassword 66 | # value in the Chocolatey config file 67 | [Parameter()] 68 | [String] 69 | $ServiceCommunicationSalt = '{{ ServiceSaltValue }}', 70 | 71 | [Parameter(Mandatory)] 72 | [PSCredential] 73 | $RepositoryCredential, 74 | 75 | # The URL of a proxy server to use for connecting to the repository. 76 | [Parameter()] 77 | [String] 78 | $ProxyUrl, 79 | 80 | # The credentials, if required, to connect to the proxy server. 81 | [Parameter()] 82 | [PSCredential] 83 | $ProxyCredential, 84 | 85 | #Install the Chocolatey Licensed Extension with right-click context menus available 86 | [Parameter()] 87 | [Switch] 88 | $IncludePackageTools, 89 | 90 | # Allows for the application of user-defined configuration that is applied after the base configuration. 91 | # Can override base configuration with this parameter 92 | [Parameter()] 93 | [Hashtable] 94 | $AdditionalConfiguration, 95 | 96 | # Allows for the toggling of additonal features that is applied after the base configuration. 97 | # Can override base configuration with this parameter 98 | [Parameter()] 99 | [Hashtable] 100 | $AdditionalFeatures, 101 | 102 | # Allows for the installation of additional packages after the system base packages have been installed. 103 | [Parameter()] 104 | [Hashtable[]] 105 | $AdditionalPackages, 106 | 107 | # Allows for the addition of alternative sources after the base conifguration has been applied. 108 | # Can override base configuration with this parameter 109 | [Parameter()] 110 | [Hashtable[]] 111 | $AdditionalSources, 112 | 113 | # If passed, downloads the certificate from the client server before initializing Chocolatey Agent 114 | [Parameter()] 115 | [Switch] 116 | $TrustCertificate 117 | ) 118 | 119 | # Touch NOTHING below this line 120 | begin { 121 | 122 | $params = $PSCmdlet.MyInvocation.BoundParameters 123 | 124 | $commonParameters = @( 125 | 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 126 | 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable') 127 | 128 | $PSCmdlet.MyInvocation.MyCommand.Parameters.Keys | ForEach-Object { 129 | if ((-not $params.ContainsKey($_)) -and ($_ -notin $commonParameters)) { 130 | $params[$_] = (Get-Variable -Name $_ -Scope 0 -ErrorAction SilentlyContinue).Value 131 | } 132 | } 133 | # Set up our downloader 134 | $downloader = [System.Net.WebClient]::new() 135 | 136 | # Setup proxy if required 137 | if ($ProxyUrl) { 138 | $proxy = [System.Net.WebProxy]::new($ProxyUrl, $true <#bypass on local#>) 139 | 140 | if ($ProxyCredential) { 141 | $proxy.Credentials = $ProxyCredential 142 | } 143 | 144 | $downloader.Proxy = $proxy 145 | } 146 | 147 | $downloader.Credentials = $RepositoryCredential 148 | 149 | } 150 | 151 | end { 152 | # If we use a Self-Signed certificate, we need to explicitly trust it 153 | if ($TrustCertificate) { 154 | Invoke-Expression ($downloader.DownloadString("http://$($Fqdn):80/Import-ChocoServerCertificate.ps1")) 155 | } 156 | 157 | # Once we trust the SSL certificate, we can start onboarding 158 | $RepositoryUrl = "https://$($fqdn):8443/repository/ChocolateyInternal/index.json" 159 | 160 | foreach ($Parameter in @("FQDN", "TrustCertificate")) { 161 | $null = $params.Remove($Parameter) 162 | } 163 | 164 | $params += @{ 165 | RepositoryUrl = $RepositoryUrl 166 | } 167 | 168 | $script = $downloader.DownloadString("https://$($FQDN):8443/repository/choco-install/ClientSetup.ps1") 169 | 170 | & ([scriptblock]::Create($script)) @params 171 | } -------------------------------------------------------------------------------- /scripts/Set-CCMCert.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Certificate renewal script for Chocolatey Central Management(CCM) 4 | 5 | .DESCRIPTION 6 | This script will go through and renew the certificate association with both the Chocolatey Central Management Service and IIS Web hosted dashboard. 7 | 8 | .PARAMETER CertificateThumbprint 9 | Thumbprint value of the certificate you would like the Chocolatey Central Management Service and Web to run on. 10 | Please make sure the certificate is located in both the Cert:\LocalMachine\TrustedPeople\ and Cert:\LocalMachine\My certificate stores. 11 | 12 | .EXAMPLE 13 | PS> .\Set-CCMCert.ps1 -CertificateThumbprint 'Your_Certificate_Thumbprint_Value' 14 | #> 15 | 16 | [CmdletBinding()] 17 | param( 18 | [Parameter(Mandatory)] 19 | [Alias("CertificateThumbprint")] 20 | [ArgumentCompleter({ 21 | Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { 22 | [System.Management.Automation.CompletionResult]::new( 23 | $_.Thumbprint, 24 | $_.Thumbprint, 25 | "ParameterValue", 26 | ($_.Subject -replace "^CN=(?.+),?.*$",'${FQDN}') 27 | ) 28 | } 29 | })] 30 | [String] 31 | $Thumbprint 32 | ) 33 | 34 | begin { 35 | if($host.name -ne 'ConsoleHost') { 36 | Write-Warning "This script cannot be ran from within PowerShell ISE" 37 | Write-Warning "Please launch powershell.exe as an administrator, and run this script again" 38 | break 39 | } 40 | } 41 | 42 | process { 43 | 44 | #Stop Central Management components 45 | Stop-Service chocolatey-central-management 46 | Get-Process chocolateysoftware.chocolateymanagement.web* | Stop-Process -ErrorAction SilentlyContinue -Force 47 | 48 | #Remove existing bindings 49 | Write-Verbose "Removing existing bindings" 50 | netsh http delete sslcert ipport=0.0.0.0:443 51 | 52 | #Add new CCM Web IIS Binding 53 | Write-Verbose "Adding new IIS binding to Chocolatey Central Management" 54 | $guid = [Guid]::NewGuid().ToString("B") 55 | netsh http add sslcert ipport=0.0.0.0:443 certhash=$Thumbprint certstorename=MY appid="$guid" 56 | Get-WebBinding -Name ChocolateyCentralManagement | Remove-WebBinding 57 | New-WebBinding -Name ChocolateyCentralManagement -Protocol https -Port 443 -SslFlags 0 -IpAddress '*' 58 | 59 | #Write Thumbprint to CCM Service appsettings.json 60 | $appSettingsJson = 'C:\ProgramData\chocolatey\lib\chocolatey-management-service\tools\service\appsettings.json' 61 | $json = Get-Content $appSettingsJson | ConvertFrom-Json 62 | $json.CertificateThumbprint = $Thumbprint 63 | $json | ConvertTo-Json | Set-Content $appSettingsJson -Force 64 | 65 | #Try Restarting CCM Service 66 | try { 67 | Start-Service chocolatey-central-management -ErrorAction Stop 68 | } 69 | catch { 70 | #Try again... 71 | Start-Service chocolatey-central-management -ErrorAction SilentlyContinue 72 | } 73 | finally { 74 | if ((Get-Service chocolatey-central-management).Status -ne 'Running') { 75 | Write-Warning "Unable to start Chocolatey Central Management service, please start manually in Services.msc" 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /scripts/Set-JenkinsCert.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Updates a keystore and ensure Jenkins is configured to use an appropriate port and certificate for HTTPS access 4 | 5 | .Example 6 | .\Set-JenkinsCert.ps1 -Thumbprint $Thumbprint -Port 7443 7 | 8 | .Notes 9 | Restarts the Jenkins service if it is running. 10 | #> 11 | param( 12 | # Thumbprint of the certificate stored in the Trusted People cert-store. 13 | [Parameter(Mandatory)] 14 | [Alias("CertificateThumbprint")] 15 | [ArgumentCompleter({ 16 | Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { 17 | [System.Management.Automation.CompletionResult]::new( 18 | $_.Thumbprint, 19 | $_.Thumbprint, 20 | "ParameterValue", 21 | ($_.Subject -replace "^CN=(?.+),?.*$",'${FQDN}') 22 | ) 23 | } 24 | })] 25 | [String] 26 | $Thumbprint, 27 | 28 | # Port number to use for Jenkins HTTPS. 29 | [uint16]$Port = 7443 30 | ) 31 | 32 | . $PSScriptRoot\Get-Helpers.ps1 33 | Set-JenkinsCertificate @PSBoundParameters -------------------------------------------------------------------------------- /scripts/Set-NexusCert.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Certificate renewal script for Nexus. 4 | 5 | .DESCRIPTION 6 | Helps edit the java keystore file for Nexus when doing a certificate renewal. 7 | 8 | .PARAMETER Thumbprint 9 | Thumbprint value of certificate you want to run Nexus on. Make sure certificate is located at Cert:\LocalMachine\TrustedPeople\ 10 | 11 | .PARAMETER NexusPort 12 | Port you have Nexus configured to run on. 13 | 14 | .EXAMPLE 15 | PS> .\Set-NexusCert.ps1 -Thumbprint 'Your_Certificate_Thumbprint_Value' -NexusPort 'Port_Number' 16 | #> 17 | 18 | [CmdletBinding()] 19 | param( 20 | [Parameter(Mandatory)] 21 | [Alias("CertificateThumbprint")] 22 | [ArgumentCompleter({ 23 | Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { 24 | [System.Management.Automation.CompletionResult]::new( 25 | $_.Thumbprint, 26 | $_.Thumbprint, 27 | "ParameterValue", 28 | ($_.Subject -replace "^CN=(?.+),?.*$",'${FQDN}') 29 | ) 30 | } 31 | })] 32 | [String] 33 | $Thumbprint, 34 | 35 | [Parameter()] 36 | [string] 37 | $NexusPort = '8443' 38 | ) 39 | 40 | begin { 41 | if($host.name -ne 'ConsoleHost') { 42 | Write-Warning "This script cannot be ran from within PowerShell ISE" 43 | Write-Warning "Please launch powershell.exe as an administrator, and run this script again" 44 | break 45 | } 46 | } 47 | 48 | process { 49 | 50 | $ErrorActionPreference = 'Stop' 51 | 52 | if ((Test-Path C:\ProgramData\nexus\etc\ssl\keystore.jks)) { 53 | Remove-Item C:\ProgramData\nexus\etc\ssl\keystore.jks -Force 54 | } 55 | 56 | $KeyTool = "C:\ProgramData\nexus\jre\bin\keytool.exe" 57 | $password = "chocolatey" | ConvertTo-SecureString -AsPlainText -Force 58 | $certificate = Get-ChildItem Cert:\LocalMachine\TrustedPeople\ | Where-Object { $_.Thumbprint -eq $Thumbprint } | Sort-Object | Select-Object -First 1 59 | 60 | Write-Host "Exporting .pfx file to C:\, will remove when finished" -ForegroundColor Green 61 | $certificate | Export-PfxCertificate -FilePath C:\cert.pfx -Password $password 62 | Get-ChildItem -Path c:\cert.pfx | Import-PfxCertificate -CertStoreLocation Cert:\LocalMachine\My -Exportable -Password $password 63 | Write-Warning -Message "You'll now see prompts and other outputs, things are working as expected, don't do anything" 64 | $string = ("chocolatey" | & $KeyTool -list -v -keystore C:\cert.pfx -J"-Duser.language=en") -match '^Alias.*' 65 | $currentAlias = ($string -split ':')[1].Trim() 66 | 67 | $passkey = '9hPRGDmfYE3bGyBZCer6AUsh4RTZXbkw' 68 | & $KeyTool -importkeystore -srckeystore C:\cert.pfx -srcstoretype PKCS12 -srcstorepass chocolatey -destkeystore C:\ProgramData\nexus\etc\ssl\keystore.jks -deststoretype JKS -alias $currentAlias -destalias jetty -deststorepass $passkey 69 | & $KeyTool -keypasswd -keystore C:\ProgramData\nexus\etc\ssl\keystore.jks -alias jetty -storepass $passkey -keypass chocolatey -new $passkey 70 | 71 | $xmlPath = 'C:\ProgramData\nexus\etc\jetty\jetty-https.xml' 72 | [xml]$xml = Get-Content -Path 'C:\ProgramData\nexus\etc\jetty\jetty-https.xml' 73 | foreach ($entry in $xml.Configure.New.Where{ $_.id -match 'ssl' }.Set.Where{ $_.name -match 'password' }) { 74 | $entry.InnerText = $passkey 75 | } 76 | 77 | $xml.OuterXml | Set-Content -Path $xmlPath 78 | 79 | Remove-Item C:\cert.pfx 80 | 81 | $nexusPath = 'C:\ProgramData\sonatype-work\nexus3' 82 | $configPath = "$nexusPath\etc\nexus.properties" 83 | 84 | (Get-Content $configPath) | Where-Object {$_ -notmatch "application-port-ssl="} | Set-Content $configPath 85 | 86 | $configStrings = @('jetty.https.stsMaxAge=-1', "application-port-ssl=$NexusPort", 'nexus-args=${jetty.etc}/jetty.xml,${jetty.etc}/jetty-https.xml,${jetty.etc}/jetty-requestlog.xml') 87 | $configStrings | ForEach-Object { 88 | if ((Get-Content -Raw $configPath) -notmatch [regex]::Escape($_)) { 89 | $_ | Add-Content -Path $configPath 90 | } 91 | } 92 | 93 | Restart-Service nexus 94 | 95 | Write-Host -BackgroundColor Black -ForegroundColor DarkGreen "The script has successfully run and the Nexus service is now rebooting for the changes to take effect." 96 | } -------------------------------------------------------------------------------- /tests/ccm.tests.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param( 3 | [Parameter(Mandatory)] 4 | [String] 5 | $Fqdn 6 | ) 7 | 8 | Describe "Chocolatey Central Management Configuration" { 9 | Context "Services" { 10 | BeforeAll { 11 | $expectedCertificate = Get-ChildItem Cert:\LocalMachine\TrustedPeople | Where-Object { "CN=$Fqdn" -like $_.Subject } 12 | $centralManagementServiceCertificate = Get-RemoteCertificate -Computername $Fqdn -Port 24020 13 | $centralManagementWebCertificate = Get-RemoteCertificate -ComputerName $Fqdn -Port 443 14 | 15 | $centralManagementFirewallRule = (Get-NetFirewallRule -DisplayName Choco*) 16 | 17 | $CCMService = try { 18 | ([System.Net.WebRequest]::Create("https://$($Fqdn):24020/ChocolateyManagementService") -as [System.net.HttpWebRequest]).GetResponse().StatusCode 19 | } 20 | catch { 21 | $_.Exception.Message -match '400' 22 | } 23 | } 24 | 25 | It "Website is listening on port 443" { 26 | ([System.Net.WebRequest]::Create("https://$($Fqdn)") -as [System.net.HttpWebRequest]).GetResponse().StatusCode -eq 'OK' | Should -Be $true 27 | } 28 | It "Service is listening on port 24020" { 29 | $CCMService | Should -Be $true 30 | } 31 | It "Web interface is using correct SSL Certificate" { 32 | $centralManagementWebCertificate | Should -Be $expectedCertificate 33 | } 34 | It "Central Management service is using correct SSL Certificate" { 35 | $centralManagementServiceCertificate | Should -Be $expectedCertificate 36 | } 37 | It "Firewall rule for Central Management Service exists" { 38 | $centralManagementFirewallRule | Should -Not -BeNullOrEmpty 39 | } 40 | It "Firewall rule for Central Management Service is enabled" { 41 | $centralManagementFirewallRule.Enabled | Should -Be $true 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /tests/jenkins.tests.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param( 3 | [Parameter(Mandatory)] 4 | [String] 5 | $Fqdn 6 | ) 7 | 8 | Describe "Jenkins Configuration" { 9 | Context "Installation Integrity" { 10 | BeforeAll { 11 | $jenkins = C:\ProgramData\chocolatey\choco.exe list -r | ConvertFrom-Csv -Delimiter '|' -Header Package,Version | Where-Object Package -eq 'jenkins' 12 | $service = Get-Service jenkins 13 | } 14 | 15 | It "Jenkins is installed" { 16 | $jenkins | Should -Not -BeNullOrEmpty 17 | } 18 | 19 | It "Service is installed" { 20 | $service | Should -Not -BeNullOrEmpty 21 | } 22 | 23 | It "Service is running" { 24 | $service.Status | Should -Be 'Running' 25 | } 26 | 27 | } 28 | 29 | Context "Required Scripts" { 30 | BeforeAll { 31 | $Scripts = (Get-ChildItem 'C:\Scripts' -Recurse -Filter *.ps1).Name 32 | } 33 | 34 | It "Get-UpdatedPackage.ps1 is present" { 35 | 'Get-UpdatedPackage.ps1' -in $Scripts | Should -Be $true 36 | } 37 | 38 | It "Invoke-ChocolateyInternalizer.ps1 is present" { 39 | 'Invoke-ChocolateyInternalizer.ps1' -in $Scripts | Should -Be $true 40 | } 41 | 42 | It "Update-ProdRepoFromTest.ps1 is present" { 43 | 'Update-ProdRepoFromTest.ps1' -in $Scripts | Should -Be $true 44 | } 45 | } 46 | 47 | Context "Required Jobs" { 48 | BeforeAll { 49 | $jobs = (Get-ChildItem 'C:\ProgramData\Jenkins\.jenkins\jobs\' -Directory).Name 50 | } 51 | 52 | It "'Internalize packages from the Community Repository' is present" { 53 | 'Internalize packages from the Community Repository' -in $jobs | Should -Be $true 54 | } 55 | 56 | It "'Update Production Repository' is present" { 57 | 'Update Production Repository' -in $jobs | Should -Be $true 58 | } 59 | 60 | It "'Update test repository from Chocolatey Community Repository' is present" { 61 | 'Update test repository from Chocolatey Community Repository' -in $jobs | Should -Be $true 62 | } 63 | } 64 | 65 | Context "Web Interface" { 66 | It "Jenkins Web UI should be available" { 67 | ([System.Net.WebRequest]::Create("https://$($Fqdn):7443/login?from=%2F") -as [System.net.HttpWebRequest]).GetResponse().StatusCode -eq 'OK' | Should -Be $true 68 | } 69 | } 70 | 71 | Context "Required Plugins" { 72 | BeforeDiscovery { 73 | $ExpectedPlugins = (Get-Content $PSScriptRoot\..\files\jenkins.json | ConvertFrom-Json).plugins.name 74 | } 75 | 76 | BeforeAll { 77 | $plugins = (Get-ChildItem 'C:\ProgramData\Jenkins\.jenkins\plugins\' -Directory).Name 78 | } 79 | 80 | It "<_> plugin is installed" -ForEach $ExpectedPlugins { 81 | $_ -in $plugins | Should -be $true 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /tests/nexus.tests.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param( 3 | [Parameter(Mandatory)] 4 | [String] 5 | $Fqdn 6 | ) 7 | 8 | . $PSScriptRoot/packages.ps1 9 | 10 | Describe "Nexus Configuration" { 11 | Context "Installation Integrity" { 12 | BeforeAll { 13 | $nexus = C:\ProgramData\chocolatey\choco.exe list -r | ConvertFrom-Csv -Delimiter '|' -Header Package, Version | Where-Object Package -EQ nexus-repository 14 | $service = Get-Service nexus 15 | } 16 | 17 | It "Nexus package should be installed" { 18 | $nexus | Should -Not -BeNullOrEmpty 19 | } 20 | 21 | It "Nexus service is installed" { 22 | $service | Should -Not -BeNullOrEmpty 23 | } 24 | 25 | It "Nexus service is running" { 26 | $service.Status | Should -Be 'Running' 27 | } 28 | } 29 | Context "Services" { 30 | BeforeAll { 31 | $certStoreCertificate = Get-ChildItem Cert:\LocalMachine\TrustedPeople | Where-Object { "CN=$Fqdn" -like $_.Subject } 32 | $serviceCertificate = Get-RemoteCertificate -ComputerName $Fqdn -Port 8443 33 | 34 | $ConfigurationFile = Get-Content "C:\ProgramData\sonatype-work\nexus3\etc\nexus.properties" 35 | } 36 | 37 | It "Service has HSTS disabled" { 38 | $ConfigurationFile -contains 'jetty.https.stsMaxAge=-1' | Should -Be $true 39 | } 40 | 41 | It "Service is using port 8443" { 42 | $ConfigurationFile -contains 'application-port-ssl=8443' | Should -Be $true 43 | } 44 | 45 | It "Service is using jetty-https.xml" { 46 | $ConfigurationFile -contains 'nexus-args=${jetty.etc}/jetty.xml,${jetty.etc}/jetty-https.xml,${jetty.etc}/jetty-requestlog.xml' | Should -Be $true 47 | } 48 | 49 | It "Service is using the appropriate SSL Certificate" { 50 | $certStoreCertificate -eq $serviceCertificate | Should -Be $true 51 | } 52 | 53 | It "Service responds to web requests" { 54 | ([System.Net.WebRequest]::Create("https://$($Fqdn):8443") -as [System.net.HttpWebRequest]).GetResponse().StatusCode -eq 'OK' | Should -Be $true 55 | } 56 | } 57 | 58 | Context "Repository Configuration" { 59 | BeforeAll { 60 | $credential = Get-ChocoEnvironmentProperty NexusCredential 61 | . "C:\choco-setup\files\scripts\Get-Helpers.ps1" 62 | $null = Connect-NexusServer -Hostname $Fqdn -Credential $credential -UseSSL 63 | 64 | $repositories = Get-NexusRepository 65 | } 66 | It "ChocolateyInternal" { 67 | 'ChocolateyInternal' -in $repositories.Name | Should -Be $true 68 | } 69 | 70 | It "ChocolateyTest" { 71 | 'ChocolateyTest' -in $repositories.Name | Should -Be $true 72 | } 73 | 74 | It "choco-install" { 75 | 'choco-install' -in $repositories.Name | Should -Be $true 76 | } 77 | 78 | It 'NuGet API-Key Realm is active' { 79 | 'NuGetApiKey' -in (Get-NexusRealm -Active).id | Should -Be $true 80 | } 81 | } 82 | 83 | Context "Package Availability" { 84 | BeforeAll { 85 | if (([version] (C:\ProgramData\chocolatey\choco.exe --version).Split('-')[0]) -ge [version] '2.1.0') { 86 | C:\ProgramData\chocolatey\choco.exe cache remove 87 | } 88 | 89 | $packages = C:\ProgramData\chocolatey\choco.exe search -s ChocolateyInternal -r | ConvertFrom-Csv -Delimiter '|' -Header Package, Version 90 | } 91 | 92 | It " is in the repository" -ForEach @( $JointPackages + $RepositoryOnlyPackages ) { 93 | $Name -in $packages.Package | Should -Be $true 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/packages.ps1: -------------------------------------------------------------------------------- 1 | $JointPackages = @( 2 | @{Name = 'chocolatey-agent' } 3 | @{Name = 'chocolatey-core.extension' } 4 | @{Name = 'chocolatey-license' } 5 | @{Name = 'chocolatey-management-database' } 6 | @{Name = 'chocolatey-management-service' } 7 | @{Name = 'chocolatey-management-web' } 8 | @{Name = 'chocolatey.extension' } 9 | @{Name = 'chocolatey' } 10 | @{Name = 'KB2919355' } 11 | @{Name = 'KB2919442' } 12 | ) 13 | $ServerOnlyPackages = @( 14 | @{Name = 'chocolatey-windowsupdate.extension' } 15 | @{Name = 'dotnet-8.0-aspnetruntime' } 16 | @{Name = 'dotnet-8.0-runtime' } 17 | @{Name = 'dotnet-aspnetcoremodule-v2' } 18 | @{Name = 'jenkins' } 19 | @{Name = 'KB2999226' } 20 | @{Name = 'KB3033929' } 21 | @{Name = 'KB3035131' } 22 | @{Name = 'nexus-repository' } 23 | @{Name = 'Pester' } 24 | @{Name = 'sql-server-express' } 25 | @{Name = 'Temurin21jre' } 26 | @{Name = 'vcredist140' } 27 | ) 28 | $RepositoryOnlyPackages = @( 29 | @{Name = 'chocolatey-dotnetfx.extension' } 30 | @{Name = 'chocolateygui' } 31 | @{Name = 'chocolateygui.extension' } 32 | @{Name = 'dotnetfx' } 33 | ) 34 | -------------------------------------------------------------------------------- /tests/server.tests.ps1: -------------------------------------------------------------------------------- 1 | . $PSScriptRoot/packages.ps1 2 | Describe "Server Integrity" { 3 | Context "Chocolatey Sources" { 4 | BeforeAll { 5 | $sources = C:\ProgramData\chocolatey\choco.exe source list -r | ConvertFrom-Csv -Delimiter '|' -Header Name, Url, Disabled, Username, Password, Priority, BypassProxy, SelfService, AdminOnly 6 | } 7 | 8 | It "LocalChocolateySetup source was removed" { 9 | "LocalChocolateySetup" -in $sources.Name | Should -BeFalse 10 | } 11 | 12 | It "ChocolateyInternal source exists" { 13 | "ChocolateyInternal" -in $sources.Name | Should -Be $true 14 | } 15 | } 16 | 17 | Context "Required Packages" { 18 | BeforeAll { 19 | $packages = C:\ProgramData\chocolatey\choco.exe list -r | ConvertFrom-Csv -Delimiter '|' -Header Package, Version 20 | } 21 | 22 | It " is installed" -Foreach @( $JointPackages + $ServerOnlyPackages ) { 23 | $Name -in $packages.Package | Should -Be $true 24 | } 25 | } 26 | 27 | Context "Readme File" { 28 | It "Readme file was created" { 29 | Test-Path (Join-Path "$env:PUBLIC\Desktop" -ChildPath 'Readme.html') | Should -Be $true 30 | } 31 | } 32 | 33 | Context "Server Roles and Features" { 34 | It "Web Server role is installed" { 35 | (Get-WindowsFeature -Name Web-Server).Installed | Should -Be $true 36 | } 37 | 38 | It "IIS Application Init Role is installed" { 39 | (Get-WindowsFeature -Name Web-AppInit).Installed | Should -Be $true 40 | } 41 | } 42 | 43 | Context "Log Files" { 44 | BeforeAll { 45 | $Logs = Get-ChildItem C:\choco-setup\logs -Recurse -Filter *.txt 46 | } 47 | 48 | It "<_> log file was created during installation" -ForEach @( 49 | 'Start-C4bCcmSetup' 50 | 'Start-C4bJenkinsSetup' 51 | 'Start-C4bNexusSetup' 52 | 'Initialize-C4bSetup' 53 | ) { 54 | Test-Path "C:\choco-setup\logs\$($_)*.txt" | Should -Be $true 55 | } 56 | } 57 | } 58 | --------------------------------------------------------------------------------