diff --git a/Start-C4bSetup.ps1 b/Initialize-C4bSetup.ps1 similarity index 50% rename from Start-C4bSetup.ps1 rename to Initialize-C4bSetup.ps1 index 9205514..dbcdbd7 100644 --- a/Start-C4bSetup.ps1 +++ b/Initialize-C4bSetup.ps1 @@ -10,15 +10,14 @@ C4B Quick-Start Guide initial bootstrap script - Setup of local `choco-setup` directories - Download of Chocolatey packages required for setup #> -[CmdletBinding(DefaultParameterSetName="Attended")] +[CmdletBinding(DefaultParameterSetName = 'Prepare')] param( # Full path to Chocolatey license file. # Accepts any file, and moves and renames it correctly. # You can either define this as a parameter, or # script will prompt you for it. # Script will also validate expiry. - [Parameter(ParameterSetName='Unattended')] - [Parameter(ParameterSetName='Attended')] + [Parameter(ParameterSetName = 'Install')] [string] $LicenseFile = $( if (Test-Path $PSScriptRoot\files\chocolatey.license.xml) { @@ -41,49 +40,70 @@ param( } ), - # Unattended mode. Allows you to skip running the other scripts indiviually. - [Parameter(Mandatory, ParameterSetName='Unattended')] - [switch] - $Unattend, - # Specify a credential used for the ChocolateyManagement DB user. - # Only required in Unattend mode for the CCM setup script. + # Only required in install mode for the CCM setup script. # If not populated, the script will prompt for credentials. - [Parameter(ParameterSetName='Unattended')] + [Parameter(ParameterSetName = 'Install')] [System.Management.Automation.PSCredential] $DatabaseCredential = $( - if ($PSCmdlet.ParameterSetName -eq 'Unattended') { - $Wshell = New-Object -ComObject Wscript.Shell - $null = $Wshell.Popup('You will now create a credential for the ChocolateyManagement DB user, to be used by CCM (document this somewhere).') - Get-Credential -UserName ChocoUser -Message 'Create a credential for the ChocolateyManagement DB user' + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseUser) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseUser + } elseif ($PSCmdlet.ParameterSetName -eq 'Install') { + [PSCredential]::new( + "chocodbuser", + (ConvertTo-SecureString "$(New-Guid)-$(New-Guid)" -Force -AsPlainText) + ) } ), # The certificate thumbprint that identifies the target SSL certificate in # the local machine certificate stores. - # Only used in Unattend mode for the SSL setup script. - [Parameter(ParameterSetName='Unattended')] + # Only used in install mode for the SSL setup script. + [Parameter(ParameterSetName = 'Install')] [ArgumentCompleter({ - Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { - [System.Management.Automation.CompletionResult]::new( - $_.Thumbprint, - $_.Thumbprint, - "ParameterValue", - ($_.Subject -replace "^CN=(?.+),?.*$",'${FQDN}') - ) + Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { + [System.Management.Automation.CompletionResult]::new( + $_.Thumbprint, + $_.Thumbprint, + "ParameterValue", + ($_.Subject -replace "^CN=(?.+),?.*$", '${FQDN}') + ) + } + })] + [string] + $Thumbprint = $( + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint + } else { + Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { + $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed + } | Select-Object -ExpandProperty Thumbprint -First 1 } - })] + ), + + # If using a wildcard certificate, provide a DNS name you want to use to access services secured by the certificate.\ + [Parameter(ParameterSetName = 'Install')] + [Alias("FQDN")] [string] - $Thumbprint, + $CertificateDnsName = $( + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertSubject) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertSubject + } + ), # If provided, shows all Chocolatey output. Otherwise, blissful quiet. - [switch]$ShowChocoOutput, + [switch] + $ShowChocoOutput, # The branch or Pull Request to download the C4B setup scripts from. # Defaults to main. - [string] [Alias('PR')] - $Branch = $env:CHOCO_QSG_BRANCH + [string] + $Branch = $env:CHOCO_QSG_BRANCH, + + # If provided, will skip launching the browser at the end of setup. + [Parameter(ParameterSetName = 'Install')] + [switch]$SkipBrowserLaunch ) if ($ShowChocoOutput) { $global:PSDefaultParameterValues["Invoke-Choco:InformationAction"] = "Continue" @@ -102,7 +122,7 @@ $QsRepo = if ($Branch) { } $DefaultEap, $ErrorActionPreference = $ErrorActionPreference, 'Stop' -Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Start-C4bSetup-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" +Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Initialize-C4bSetup-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" try { # Setup initial choco-setup directories @@ -114,7 +134,7 @@ try { $TestDir = Join-Path $ChocoPath "tests" $xmlDir = Join-Path $ChocoPath "clixml" - @($ChocoPath, $FilesDir, $PkgsDir, $TempDir, $TestDir,$xmlDir) | ForEach-Object { + @($ChocoPath, $FilesDir, $PkgsDir, $TempDir, $TestDir, $xmlDir) | ForEach-Object { $null = New-Item -Path $_ -ItemType Directory -Force -ErrorAction Stop } @@ -132,7 +152,7 @@ try { # Add the Module Path and Import Helper Functions if (-not (Get-Module C4B-Environment -ListAvailable)) { if ($env:PSModulePath.Split(';') -notcontains "$FilesDir\modules") { - [Environment]::SetEnvironmentVariable("PSModulePath", "$env:PSModulePath;$FilesDir\modules" ,"Machine") + [Environment]::SetEnvironmentVariable("PSModulePath", "$env:PSModulePath;$FilesDir\modules" , "Machine") $env:PSModulePath = [Environment]::GetEnvironmentVariables("Machine").PSModulePath } } @@ -144,30 +164,73 @@ try { & $FilesDir\OfflineInstallPreparation.ps1 -LicensePath $LicenseFile - if (Test-Path $FilesDir\files\*.nupkg) { - Invoke-Choco source add --name LocalChocolateySetup --source $FilesDir\files\ --Priority 1 - } + # Kick off unattended running of remaining setup scripts, if we're running from a saved-script. + if ($PSScriptRoot -or $PSCmdlet.ParameterSetName -eq 'Install') { + Update-Clixml -Properties @{ + InitialDeployment = Get-Date + } - # Set Choco Server Chocolatey Configuration - Invoke-Choco feature enable --name="'excludeChocolateyPackagesDuringUpgradeAll'" - Invoke-Choco feature enable --name="'usePackageHashValidation'" + if ($Thumbprint) { + Set-ChocoEnvironmentProperty CertThumbprint $Thumbprint + + if ($CertificateDnsName) { + Set-ChocoEnvironmentProperty CertSubject $CertificateDnsName + } + + # Collect current certificate configuration + $Certificate = Get-Certificate -Thumbprint $Thumbprint + Copy-CertToStore -Certificate $Certificate + + $null = Test-CertificateDomain -Thumbprint $Thumbprint + } elseif ($PSScriptRoot) { + # We're going to be using a self-signed certificate + if (-not $CertificateDnsName) { + $CertificateDnsName = $env:ComputerName + } + + $CertificateArgs = @{ + CertStoreLocation = "Cert:\LocalMachine\My" + KeyUsage = "KeyEncipherment", "DigitalSignature" + DnsName = $CertificateDnsName + NotAfter = (Get-Date).AddYears(10) + } + + $Certificate = New-SelfSignedCertificate @CertificateArgs + Copy-CertToStore -Certificate $Certificate + + $Thumbprint = $Certificate.Thumbprint + + Set-ChocoEnvironmentProperty CertThumbprint $Thumbprint + Set-ChocoEnvironmentProperty CertSubject $CertificateDnsName + } - # Convert license to a "choco-license" package, and install it locally to test - Write-Host "Creating a 'chocolatey-license' package, and testing install." -ForegroundColor Green - Set-Location $FilesDir - .\scripts\Create-ChocoLicensePkg.ps1 - Remove-Item "$env:SystemDrive\choco-setup\packaging" -Recurse -Force + if ($DatabaseCredential) { + Set-ChocoEnvironmentProperty DatabaseUser $DatabaseCredential + } + + if (Test-Path $FilesDir\files\*.nupkg) { + Invoke-Choco source add --name LocalChocolateySetup --source $FilesDir\files\ --Priority 1 + } + + # Set Choco Server Chocolatey Configuration + Invoke-Choco feature enable --name="'excludeChocolateyPackagesDuringUpgradeAll'" + Invoke-Choco feature enable --name="'usePackageHashValidation'" + + # Convert license to a "choco-license" package, and install it locally to test + Write-Host "Creating a 'chocolatey-license' package, and testing install." -ForegroundColor Green + Set-Location $FilesDir + .\scripts\Create-ChocoLicensePkg.ps1 + Remove-Item "$env:SystemDrive\choco-setup\packaging" -Recurse -Force - # Kick off unattended running of remaining setup scripts. - if ($Unattend) { $Certificate = @{} - if ($Thumbprint) {$Certificate.Thumbprint = $Thumbprint} + if ($Thumbprint) { $Certificate.Thumbprint = $Thumbprint } Set-Location "$env:SystemDrive\choco-setup\files" - .\Start-C4BNexusSetup.ps1 + .\Start-C4BNexusSetup.ps1 @Certificate .\Start-C4bCcmSetup.ps1 @Certificate -DatabaseCredential $DatabaseCredential - .\Start-C4bJenkinsSetup.ps1 - .\Set-SslSecurity.ps1 @Certificate + .\Start-C4bJenkinsSetup.ps1 @Certificate + + Complete-C4bSetup -SkipBrowserLaunch:$SkipBrowserLaunch } } finally { $ErrorActionPreference = $DefaultEap diff --git a/README.md b/README.md index bb5459a..8bef2c3 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Below is the Quick Start Guide as it exists currently on the [Chocolatey Docs](h --- -Welcome to the Chocolatey of Business (C4B) Quick-Start Guide! This guide will walk you through the basics of configuring a C4B Server on your VM infrastructure of choice. This includes: +Welcome to the Chocolatey for Business (C4B) Quick-Start Guide! This guide will walk you through the basics of configuring a C4B Server on your VM infrastructure of choice. This includes: - The Chocolatey Licensed components - A NuGet V3 Repository (Nexus) @@ -64,7 +64,7 @@ Below are the minimum requirements for setting up your C4B server via this guide 1. If you plan on joining this server to your Active Directory domain, do so now before beginning setup below. -1. If you plan to use a Purchased/Acquired or Domain SSL certificate, please ensure the CN/Subject value matches the DNS-resolvable Fully Qualified Domain Name (FQDN) of your C4B Server. Place this certificate in the `Local Machine > Personal` certificate store, and ensure that the private key is exportable. +1. If you plan to use a Purchased/Acquired or Domain SSL certificate, please ensure the CN/Subject value matches the DNS-resolvable Fully Qualified Domain Name (FQDN) of your C4B Server. Place this certificate in the `Local Machine > Trusted People` certificate store, and ensure that the private key is exportable. 1. Copy your `chocolatey.license.xml` license file (from the email you received) onto your C4B Server. @@ -90,6 +90,8 @@ Below are the minimum requirements for setting up your C4B server via this guide Set-ExecutionPolicy Bypass -Scope Process -Force [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::tls12 Invoke-RestMethod https://ch0.co/qsg-go | Invoke-Expression + Set-Location "$env:SystemDrive\choco-setup\files" + .\Initialize-C4bSetup.ps1 ``` >
@@ -106,144 +108,116 @@ Below are the minimum requirements for setting up your C4B server via this guide > :memo:**Offline Install**: You can now copy the `C:\choco-setup\` directory to any computer to continue the installation. To zip up that directory, run `Compress-Archive -Path C:\choco-setup\files\* -DestinationPath C:\choco-setup\C4B-Files.zip`. Move the archive to your new machine, and run `Expand-Archive -Path /path/to/C4B-Files.zip -DestinationPath C:\choco-setup\files -Force`. You should then run `Set-Location "$env:SystemDrive\choco-setup\files"; .\Start-C4bSetup.ps1`, and continue with the guide. -### Step 2: Nexus Setup - -1. In the same **elevated** Windows PowerShell console as above, paste and run the following code: - - ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Start-C4bNexusSetup.ps1 - ``` - - >
- > What does this script do? (click to expand) - >
    - >
  • Installs Sonatype Nexus Repository Manager OSS instance
  • - >
  • Cleans up all demo repositories on Nexus
  • - >
  • Creates a "ChocolateyInternal" NuGet repository
  • - >
  • Creates a "ChocolateyTest" NuGet repository
  • - >
  • Creates a "choco-install" raw repository
  • - >
  • Sets up "ChocolateyInternal" on C4B Server as source, with API key
  • - >
  • Adds firewall rule for repository access
  • - >
  • Installs MS Edge, as Internet Explorer cannot access the Sonatype Nexus site
  • - >
  • Outputs data to a JSON file to pass between scripts
  • - >
- >
- -### Step 3: Chocolatey Central Management Setup - -1. In the same PowerShell Administrator console as above, paste and run the following code: - - ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Start-C4bCcmSetup.ps1 - ``` - - >
- > What does this script do? (click to expand) - >
    - >
  • Installs MS SQL Express and SQL Server Management Studio (SSMS)
  • - >
  • Creates "ChocolateyManagement" database, and adds appropriate `ChocoUser` permissions
  • - >
  • Installs all 3 Chocolatey Central Management packages (database, service, web), with correct parameters
  • - >
  • Outputs data to a JSON file to pass between scripts
  • - >
- >
- -### Step 4: Jenkins Setup - -1. In the same **elevated** PowerShell console as above, paste and run the following code: - - ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Start-C4bJenkinsSetup.ps1 - ``` - - >
- > What does this script do? (click to expand) - >
    - >
  • Installs Jenkins package
  • - >
  • Updates Jenkins plugins
  • - >
  • Configures pre-downloaded Jenkins scripts for Package Internalizer automation
  • - >
  • Sets up pre-defined Jenkins jobs for the scripts above
  • - >
- >
- -### Step 5: SSL Setup +#### Running with a Certificate -1. In the same **elevated** PowerShell console as above, paste and run the following code: +**ALTERNATIVE 1 : Custom SSL Certificate** - If you have your own custom SSL certificate (purchased/acquired, or from your Domain CA), you can paste and run the following script with the `Thumbprint` value of your SSL certificate specified: - ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Set-SslSecurity.ps1 - ``` +```powershell +Set-Location "$env:SystemDrive\choco-setup\files" +.\Initialize-C4bSetup.ps1 -Thumbprint '' +``` - **ALTERNATIVE 1 : Custom SSL Certificate** - If you have your own custom SSL certificate (purchased/acquired, or from your Domain CA), you can paste and run the following script with the `Thumbprint` value of your SSL certificate specified: +> :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. - ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Set-SslSecurity.ps1 -Thumbprint '' - ``` - - > :warning:**REMINDER**: If you are using your own SSL certificate, be sure to place this certificate in the `Local Machine > Personal` certificate store before running the above script, and ensure that the private key is exportable. - - > :memo: **NOTE** - > A Role and User credential will be configured to limit access to your Nexus repositories. As well, CCM Client and Service Salts are configured to further encrypt your connection between CCM and your endpoint clients. These additional settings are also incorporated into your `Register-C4bEndpoint.ps1` script for onboarding endpoints. - - **ALTERNATIVE 2 : Wildcard SSL Certificate** - If you have a wildcard certificate, you will also need to provide a DNS name you wish to use for that certificate: - - ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Set-SslSecurity.ps1 -Thumbprint '' -CertificateDnsName '' - ``` - - For example, with a wildcard certificate with a thumbprint of `deee9b2fabb24bdaae71d82286e08de1` you wish to use `chocolatey.foo.org`, the following would be required: - - ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Set-SslSecurity.ps1 -Thumbprint deee9b2fabb24bdaae71d82286e08de1 -CertificateDnsName chocolatey.foo.org - ``` - - >
- > What does this script do? (click to expand) - >
    - >
  • Adds SSL certificate configuration for Nexus and CCM web portals
  • - >
  • Generates a `Register-C4bEndpoint.ps1` script for you to easily set up endpoint clients
  • - >
  • Outputs data to a JSON file to pass between scripts
  • - >
  • Writes a Readme.html file to the Public Desktop with account information for C4B services
  • - >
  • Auto-opens README, CCM, Nexus, and Jenkins in your web browser
  • - >
  • Removes temporary JSON files used during provisioning
  • - >
- >
- - > :mag: **FYI**: A `Readme.html` file will now be generated on your desktop. This file contains login information for all 3 web portals (CCM, Nexus, and Jenkins). This `Readme.html`, along with all 3 web portals, will automatically be opened in your browser. - -### Step 6: Verification - -1. In the same **elevated** PowerShell console as above, paste and run the following code: - - ```powershell - Set-Location "$env:SystemDrive\choco-setup\files" - .\Start-C4bVerification.ps1 -Fqdn '' - ``` - - If you expect services to be available at `chocoserver.yourcompany.com`, then your command would look like: `.\Start-C4bVerification.ps1 -Fqdn 'chocoserver.yourcompany.com'` - - >
- > What does this script do? (click to expand) - >
    - >
  • Verifies Nexus Repository installation
  • - >
  • Verifies Central Management installation
  • - >
  • Verifies Jenkins installation
  • - >
  • Ensures system firewall is configured
  • - >
  • Ensures Windows Features are installed
  • - >
  • Ensures services are correctly configured
  • - >
  • Ensured README is created
  • - >
- >
- -### Step 7: Setting up Endpoints - -1. Find the `Register-C4bEndpoint.ps1` script in the `choco-setup\files\scripts\` directory on your C4B Server. Copy this script to your client endpoint. +> :memo: **NOTE** +> A Role and User credential will be configured to limit access to your Nexus repositories. As well, CCM Client and Service Salts are configured to further encrypt your connection between CCM and your endpoint clients. These additional settings are also incorporated into your `Register-C4bEndpoint.ps1` script for onboarding endpoints. + +**ALTERNATIVE 2 : Wildcard SSL Certificate** - If you have a wildcard certificate, you will also need to provide a DNS name you wish to use for that certificate: + +```powershell +Set-Location "$env:SystemDrive\choco-setup\files" +.\Initialize-C4bSetup.ps1 -Thumbprint '' -CertificateDnsName '' +``` + +For example, with a wildcard certificate with a thumbprint of `deee9b2fabb24bdaae71d82286e08de1` you wish to use `chocolatey.foo.org`, the following would be required: + +```powershell +Set-Location "$env:SystemDrive\choco-setup\files" +.\Initialize-C4bSetup.ps1 -Thumbprint deee9b2fabb24bdaae71d82286e08de1 -CertificateDnsName chocolatey.foo.org +``` + +#### Script: Nexus Setup + +As part of the C4B setup, we install and configure Sonatype Nexus Repository, which is used for hosting your Chocolatey packages internally: + +>
+> What does this script do? (click to expand) +>
    +>
  • Installs Sonatype Nexus Repository Manager OSS instance
  • +>
  • Cleans up all demo repositories on Nexus
  • +>
  • Creates a "ChocolateyInternal" NuGet repository
  • +>
  • Creates a "ChocolateyTest" NuGet repository
  • +>
  • Creates a "choco-install" raw repository
  • +>
  • Sets up "ChocolateyInternal" on C4B Server as source, with API key
  • +>
  • Adds firewall rule for repository access
  • +>
  • Installs MS Edge, as Internet Explorer cannot access the Sonatype Nexus site
  • +>
  • Outputs data to a JSON file to pass between scripts
  • +>
+>
+ +#### Script: Chocolatey Central Management Setup + +As part of the C4B setup, we install and configure Chocolatey Central Management and associated prerequisites to manage your Chocolatey for Business nodes: + +>
+> What does this script do? (click to expand) +>
    +>
  • Installs MS SQL Express and SQL Server Management Studio (SSMS)
  • +>
  • Creates "ChocolateyManagement" database, and adds appropriate `ChocoUser` permissions
  • +>
  • Installs all 3 Chocolatey Central Management packages (database, service, web), with correct parameters
  • +>
  • Outputs data to a JSON file to pass between scripts
  • +>
+>
+ +#### Script: Jenkins Setup + +As part of the C4B setup, we install and configure Jenkins as a method for running Package Internalizer and other jobs: + +>
+> What does this script do? (click to expand) +>
    +>
  • Installs Jenkins package
  • +>
  • Updates Jenkins plugins
  • +>
  • Configures pre-downloaded Jenkins scripts for Package Internalizer automation
  • +>
  • Sets up pre-defined Jenkins jobs for the scripts above
  • +>
+>
+ +#### Script: Complete Setup + +As part of the C4B setup, we create a readme and install the Chocolatey Agent on the server: + +>
+> What does this script do? (click to expand) +>
    +>
  • Sets up Chocolatey Agent on this system
  • +>
  • Writes a Readme.html file to the Public Desktop with account information for C4B services
  • +>
  • Auto-opens README, CCM, Nexus, and Jenkins in your web browser
  • +>
+>
+ +> :mag: **FYI**: A `Readme.html` file will now be generated on your desktop. This file contains login information for all 3 web portals (CCM, Nexus, and Jenkins). This `Readme.html`, along with all 3 web portals, will automatically be opened in your browser. + +### Script: Verification + +As a part of the C4B setup, we run tests to validate that your environment is correctly configured: + +>
+> What does this script do? (click to expand) +>
    +>
  • Verifies Nexus Repository installation
  • +>
  • Verifies Central Management installation
  • +>
  • Verifies Jenkins installation
  • +>
  • Ensures system firewall is configured
  • +>
  • Ensures Windows Features are installed
  • +>
  • Ensures services are correctly configured
  • +>
  • Ensured README is created
  • +>
+>
+ +### Step 2: Setting up Endpoints + +1. Find the `Register-C4bEndpoint.ps1` script in the `C:\choco-setup\files\scripts\` directory on your C4B Server. Copy this script to your client endpoint. 1. Open an **elevated** PowerShell console on your client endpoint, and browse (`cd`) to the location you copied the script above. Paste and run the following code: @@ -267,6 +241,35 @@ Below are the minimum requirements for setting up your C4B server via this guide > >
+#### Available parameters + +* `ClientCommunicationSalt` + The salt for communication from an agent to an instance of Central Management Service. Details available in the README file on server desktop. +* `ServiceCommunicationSalt` + The salt for communication from an instance of Central Management Service to an agent. Details available in the README file on server desktop. +* `RepositoryCredential` + The credential to use to access the repository server from the endpoint. Details available in the README file on server desktop. +* `ProxyUrl` + The URL of a proxy server to use for connecting to the repository. +* `ProxyCredential` + The credentials, if required, to connect to the proxy server. +* `IncludePackageTools` + Install the Chocolatey Licensed Extension with right-click context menus available. +* `AdditionalConfiguration` + Allows for the application of user-defined configuration that is applied after the base configuration. +* `AdditionalFeatures` + Allows for the toggling of additional features that is applied after the base configuration. +* `AdditionalPackages` + Allows for the installation of additional packages after the system base packages have been installed. +* `AdditionalSources` + Allows for the addition of alternative sources after the base configuration has been applied. +* `TrustCertificate` + If passed, downloads the certificate from the client server before initializing Chocolatey Agent. + +#### Advanced Endpoint Configuration + +It is possible to customize the installation of Chocolatey on an endpoint via the available parameters above. For examples, please see [Advanced Endpoint Configuration](https://docs.chocolatey.org/en-us/c4b-environments/quick-start-environment/advanced-client-configuration/). + ### Conclusion Congratulations! If you followed all the steps detailed above, you should now have a fully functioning Chocolatey for Business implementation deployed in your environment. diff --git a/Set-SslSecurity.ps1 b/Set-SslSecurity.ps1 deleted file mode 100644 index 007c019..0000000 --- a/Set-SslSecurity.ps1 +++ /dev/null @@ -1,314 +0,0 @@ -#requires -modules C4B-Environment -using namespace System.Net.Sockets -using namespace System.Net.Security -using namespace System.Security.Cryptography.X509Certificates -<# -.SYNOPSIS -Generates or retrieves certificates and generates SSL bindings for -Central Management and Nexus. - -.DESCRIPTION -Removes any existing certificates which have the subject "chocoserver" to avoid -issues, and then either generates or retrieves the required certificate. The -certificate is placed in the required local machine store, and then the script -generates SSL bindings for both Nexus and the Central Management website using the -certificate. -#> -[CmdletBinding(DefaultParameterSetName='SelfSigned')] -[OutputType([string])] -param( - # The certificate thumbprint that identifies the target SSL certificate in - # the local machine certificate stores. - # Ignored if supplied alongside -Subject. - [Parameter(ValueFromPipeline, ParameterSetName='Thumbprint')] - [ArgumentCompleter({ - Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { - [System.Management.Automation.CompletionResult]::new( - $_.Thumbprint, - $_.Thumbprint, - "ParameterValue", - ($_.Subject -replace "^CN=(?.+),?.*$",'${FQDN}') - ) - } - })] - [string] - $Thumbprint = $( - Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { - $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed - } | Select-Object -ExpandProperty Thumbprint -First 1 - ), - - # The certificate subject that identifies the target SSL certificate in - # the local machine certificate stores. - [Parameter(ParameterSetName='Subject')] - [string] - $Subject, - - # If using a wildcard certificate, provide a DNS name you want to use to access services secured by the certificate. - [Parameter(ParameterSetName='Subject')] - [Parameter(ParameterSetName='Thumbprint')] - [string] - $CertificateDnsName = $( - if (-not (Get-Command Get-ChocoEnvironmentProperty -ErrorAction SilentlyContinue)) {. $PSScriptRoot\scripts\Get-Helpers.ps1} - Get-ChocoEnvironmentProperty CertSubject - ), - - # API key of your Nexus repo, to add to the source setup on C4B Server. - [string]$NuGetApiKey = $( - if (-not (Get-Command Get-ChocoEnvironmentProperty -ErrorAction SilentlyContinue)) {. $PSScriptRoot\scripts\Get-Helpers.ps1} - Get-ChocoEnvironmentProperty NuGetApiKey -AsPlainText - ), - - # If provided, will skip launching the browser - [switch]$SkipBrowserLaunch -) -process { - $DefaultEap = $ErrorActionPreference - $ErrorActionPreference = 'Stop' - Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Set-SslCertificate-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" - - # Collect current certificate configuration - $Certificate = if ($Subject) { - Get-Certificate -Subject $Subject - } - elseif ($Thumbprint) { - Get-Certificate -Thumbprint $Thumbprint - } - - if (-not $CertificateDnsName) { - $matcher = 'CN\s?=\s?(?[^,\s]+)' - $null = $Certificate.Subject -match $matcher - $SubjectWithoutCn = if ($Matches.Subject.StartsWith('*')) { - # This is a wildcard cert, we need to prompt for the intended CertificateDnsName - while ($CertificateDnsName -notlike $Matches.Subject) { - $CertificateDnsName = Read-Host -Prompt "$(if ($CertificateDnsName) {"'$($CertificateDnsName)' is not a subdomain of '$($Matches.Subject)'. "})Please provide an FQDN to use with the certificate '$($Matches.Subject)'" - } - $CertificateDnsName - } - else { - $Matches.Subject - } - } - else { - $SubjectWithoutCn = $CertificateDnsName - } - - <# Nexus #> - # Stop Services/Processes/Websites required - Stop-Service nexus - - # Put certificate in TrustedPeople - Copy-CertToStore -Certificate $Certificate - - # Generate Nexus keystore - $null = Set-NexusCert -Thumbprint $Certificate.Thumbprint - - # Add firewall rule for Nexus - netsh advfirewall firewall add rule name="Nexus-8443" dir=in action=allow protocol=tcp localport=8443 - - Write-Verbose "Starting up Nexus" - Start-Service nexus - - Write-Warning "Waiting to give Nexus time to start up on 'https://${SubjectWithoutCn}:8443'" - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::tls12 - do { - $response = try { - Invoke-WebRequest "https://${SubjectWithoutCn}:8443" -UseBasicParsing -ErrorAction Stop - Start-Sleep -Seconds 3 - } catch {} - } until($response.StatusCode -eq '200') - Write-Host "Nexus is ready!" - - Invoke-Choco source remove --name="'ChocolateyInternal'" - - # Build Credential Object, Connect to Nexus - $securePw = (Get-Content 'C:\programdata\sonatype-work\nexus3\admin.password') | ConvertTo-SecureString -AsPlainText -Force - $Credential = [System.Management.Automation.PSCredential]::new('admin', $securePw) - - # Connect to Nexus - Connect-NexusServer -Hostname $SubjectWithoutCn -Credential $Credential -UseSSL - - # Push ClientSetup.ps1 to raw repo - $ClientScript = "$PSScriptRoot\scripts\ClientSetup.ps1" - (Get-Content -Path $ClientScript) -replace "{{hostname}}", $SubjectWithoutCn | Set-Content -Path $ClientScript - New-NexusRawComponent -RepositoryName 'choco-install' -File $ClientScript - - # Disable anonymous authentication - Set-NexusAnonymousAuth -Disabled - - if (-not (Get-NexusRole -Role 'chocorole' -ErrorAction SilentlyContinue)) { - # Create Nexus role - $RoleParams = @{ - Id = "chocorole" - Name = "chocorole" - Description = "Role for web enabled choco clients" - Privileges = @('nx-repository-view-nuget-*-browse', 'nx-repository-view-nuget-*-read', 'nx-repository-view-raw-*-read', 'nx-repository-view-raw-*-browse') - } - New-NexusRole @RoleParams - } - - if (-not (Get-NexusUser -User 'chocouser' -ErrorAction SilentlyContinue)) { - $NexusPw = [System.Web.Security.Membership]::GeneratePassword(32, 12) - # Create Nexus user - $UserParams = @{ - Username = 'chocouser' - Password = ($NexusPw | ConvertTo-SecureString -AsPlainText -Force) - FirstName = 'Choco' - LastName = 'User' - EmailAddress = 'chocouser@example.com' - Status = 'Active' - Roles = 'chocorole' - } - New-NexusUser @UserParams - } - - # Update all sources with credentials and the new path - foreach ($Repository in Get-NexusRepository -Format nuget | Where-Object Type -eq 'hosted') { - $RepositoryUrl = "https://${SubjectWithoutCn}:8443/repository/$($Repository.Name)/index.json" - - $ChocoArgs = @( - 'source', - 'add', - "--name='$($Repository.Name)'", - "--source='$RepositoryUrl'", - '--priority=1', - "--user='chocouser'", - "--password='$NexusPw'" - ) - & Invoke-Choco @ChocoArgs - - # Update Repository API key - $chocoArgs = @('apikey', "--source='$RepositoryUrl'", "--api-key='$NuGetApiKey'") - & Invoke-Choco @chocoArgs - - # Reset the NuGet v3 cache, such that it doesn't capture localhost as the FQDN - Remove-NexusRepositoryFolder -RepositoryName $Repository.Name -Name v3 - } - - Update-Clixml -Properties @{ - NexusUri = "https://$($SubjectWithoutCn):8443" - NexusRepo = "https://${SubjectWithoutCn}:8443/repository/ChocolateyInternal/index.json" - ChocoUserPassword = $NexusPw - } - - <# Jenkins #> - $JenkinsHome = "C:\ProgramData\Jenkins\.jenkins" - - # Update Jenkins Jobs with Nexus URL - Get-ChildItem -Path "$JenkinsHome\jobs" -Recurse -File -Filter 'config.xml' | Invoke-TextReplacementInFile -Replacement @{ - '(?<=https:\/\/)(?.+)(?=:8443\/repository\/)' = $SubjectWithoutCn - } - - # Generate Jenkins keystore - Set-JenkinsCertificate -Thumbprint $Certificate.Thumbprint - - # Add firewall rule for Jenkins - netsh advfirewall firewall add rule name="Jenkins-7443" dir=in action=allow protocol=tcp localport=7443 - - Update-Clixml -Properties @{ - JenkinsUri = "https://$($SubjectWithoutCn):7443" - } - - <# CCM #> - # Update the service certificate - Set-CcmCertificate -CertificateThumbprint $Certificate.Thumbprint - - # Remove old CCM web binding, and add new CCM web binding - Stop-CcmService - Remove-CcmBinding - New-CcmBinding -Thumbprint $Certificate.Thumbprint - Start-CcmService - - # Create the site hosting the certificate import script on port 80 - # Only run this if it's a self-signed cert which has 10-year validity - if ($Certificate.NotAfter -gt (Get-Date).AddYears(5)) { - $IsSelfSigned = $true - .\scripts\New-IISCertificateHost.ps1 - } - - # Generate Register-C4bEndpoint.ps1 - $EndpointScript = "$PSScriptRoot\scripts\Register-C4bEndpoint.ps1" - - $ClientSaltValue = New-CCMSalt - $ServiceSaltValue = New-CCMSalt - - Invoke-TextReplacementInFile -Path $EndpointScript -Replacement @{ - "{{ ClientSaltValue }}" = $ClientSaltValue - "{{ ServiceSaltValue }}" = $ServiceSaltValue - "{{ FQDN }}" = $SubjectWithoutCn - } - - # Agent Setup - $agentArgs = @{ - CentralManagementServiceUrl = "https://$($SubjectWithoutCn):24020/ChocolateyManagementService" - ServiceSalt = $ServiceSaltValue - ClientSalt = $ClientSaltValue - } - - if (Test-SelfSignedCertificate -Certificate $Certificate) { - # Register endpoint script - (Get-Content -Path $EndpointScript) -replace "{{hostname}}", "'$SubjectWithoutCn'" | Set-Content -Path $EndpointScript - $ScriptBlock = @" -`$downloader = New-Object -TypeName System.Net.WebClient -Invoke-Expression (`$downloader.DownloadString("http://`$(`$HostName):80/Import-ChocoServerCertificate.ps1")) -"@ - (Get-Content -Path $EndpointScript) -replace "# placeholder if using a self-signed cert", $ScriptBlock | Set-Content -Path $EndpointScript - } - - Install-ChocolateyAgent @agentArgs - - Update-Clixml -Properties @{ - CCMWebPortal = "https://$($SubjectWithoutCn)/Account/Login" - CCMServiceURL = "https://$($SubjectWithoutCn):24020/ChocolateyManagementService" - CertSubject = $SubjectWithoutCn - CertThumbprint = $Certificate.Thumbprint - CertExpiry = $Certificate.NotAfter - IsSelfSigned = $IsSelfSigned - ServiceSalt = ConvertTo-SecureString $ServiceSaltValue -AsPlainText -Force - ClientSalt = ConvertTo-SecureString $ClientSaltValue -AsPlainText -Force - } -} -end { - Write-Host 'Writing README to Desktop; this file contains login information for all C4B services.' - New-QuickstartReadme - - if (-not $SkipBrowserLaunch -and $Host.Name -eq 'ConsoleHost') { - $Message = 'The CCM, Nexus & Jenkins sites will open in your browser in 10 seconds. Press any key to skip this.' - $Timeout = New-TimeSpan -Seconds 10 - $Stopwatch = [System.Diagnostics.Stopwatch]::new() - $Stopwatch.Start() - Write-Host $Message -NoNewline -ForegroundColor Green - do { - # wait for a key to be available: - if ([Console]::KeyAvailable) { - # read the key, and consume it so it won't - # be echoed to the console: - $keyInfo = [Console]::ReadKey($true) - Write-Host "`nSkipping the Opening of sites in your browser." -ForegroundColor Green - # exit loop - break - } - # write a dot and wait a second - Write-Host '.' -NoNewline -ForegroundColor Green - Start-Sleep -Seconds 1 - } - while ($Stopwatch.Elapsed -lt $Timeout) - $Stopwatch.Stop() - - if (-not ($keyInfo)) { - Write-Host "`nOpening CCM, Nexus & Jenkins sites in your browser." -ForegroundColor Green - $Readme = 'file:///C:/Users/Public/Desktop/README.html' - $Ccm = "https://$($SubjectWithoutCn)/Account/Login" - $Nexus = "https://$($SubjectWithoutCn):8443" - $Jenkins = "https://$($SubjectWithoutCn):7443" - try { - Start-Process msedge.exe "$Readme", "$Ccm", "$Nexus", "$Jenkins" - } catch { - Start-Process chrome.exe "$Readme", "$Ccm", "$Nexus", "$Jenkins" - } - } - } - - $ErrorActionPreference = $DefaultEap - Stop-Transcript -} diff --git a/Start-C4bCcmSetup.ps1 b/Start-C4bCcmSetup.ps1 index 0c1f0cf..95f3a74 100644 --- a/Start-C4bCcmSetup.ps1 +++ b/Start-C4bCcmSetup.ps1 @@ -15,7 +15,16 @@ param( [Parameter()] [ValidateNotNull()] [System.Management.Automation.PSCredential] - $DatabaseCredential = (Get-Credential -Username ChocoUser -Message 'Create a credential for the ChocolateyManagement DB user (document this somewhere)'), + $DatabaseCredential = $( + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseUser) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).DatabaseUser + } else { + [PSCredential]::new( + "chocodbuser", + (ConvertTo-SecureString "$(New-Guid)-$(New-Guid)" -Force -AsPlainText) + ) + } + ), # Certificate to use for CCM service [Parameter()] @@ -30,8 +39,17 @@ param( ) } })] + [ValidateScript({Test-CertificateDomain -Thumbprint $_})] [String] - $Thumbprint + $Thumbprint = $( + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint + } else { + Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { + $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed + } | Select-Object -ExpandProperty Thumbprint -First 1 + } + ) ) process { $DefaultEap = $ErrorActionPreference @@ -116,7 +134,11 @@ process { & Invoke-Choco @chocoArgs Write-Host "Creating Chocolatey Central Management Database" - choco install chocolatey-management-database --source='ChocolateyInternal' -y --package-parameters="'/ConnectionString=Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;Trusted_Connection=true;'" --no-progress + $chocoArgs = @('install', 'chocolatey-management-database', '--source="ChocolateyInternal"', '-y', '--package-parameters="''/ConnectionString=Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;Trusted_Connection=true;''"', '--no-progress') + if ($PackageVersion = $Packages.Where{ $_.Name -eq 'chocolatey-management-database' }.Version) { + $chocoArgs += "--version='$($PackageVersion)'" + } + & Invoke-Choco @chocoArgs # Add Local Windows User: $DatabaseUser = $DatabaseCredential.UserName @@ -130,9 +152,13 @@ process { if (-not $hostName.EndsWith($domainName)) { $hostName += "." + $domainName } + $CcmEndpoint = "http://$hostName" Write-Host "Installing Chocolatey Central Management Service" $chocoArgs = @('install', 'chocolatey-management-service', "--source='ChocolateyInternal'", '-y', "--package-parameters-sensitive=`"/ConnectionString:'Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;User ID=$DatabaseUser;Password=$DatabaseUserPw;'`"", '--no-progress') + if ($PackageVersion = $Packages.Where{ $_.Name -eq 'chocolatey-management-service' }.Version) { + $chocoArgs += "--version='$($PackageVersion)'" + } if ($Thumbprint) { Write-Verbose "Validating certificate is in LocalMachine\TrustedPeople Store" if (-not (Get-Item Cert:\LocalMachine\TrustedPeople\$Thumbprint -EA 0) -and -not (Get-Item Cert:\LocalMachine\My\$Thumbprint -EA 0)) { @@ -148,17 +174,92 @@ process { $chocoArgs += @("--package-parameters='/CertificateThumbprint=$Thumbprint'") } & Invoke-Choco @chocoArgs + + if (-not $MyCertificate) { $MyCertificate = Get-Item Cert:\LocalMachine\My\* } Write-Host "Installing Chocolatey Central Management Website" $chocoArgs = @('install', 'chocolatey-management-web', "--source='ChocolateyInternal'", '-y', "--package-parameters-sensitive=""'/ConnectionString:Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;User ID=$DatabaseUser;Password=$DatabaseUserPw;'""", '--no-progress') + if ($PackageVersion = $Packages.Where{ $_.Name -eq 'chocolatey-management-web' }.Version) { + $chocoArgs += "--version='$($PackageVersion)'" + } & Invoke-Choco @chocoArgs + # Setup Website SSL + if ($Thumbprint) { + Stop-CcmService + Remove-CcmBinding + New-CcmBinding -Thumbprint $Thumbprint + Start-CcmService + + $CcmEndpoint = "https://$(Get-ChocoEnvironmentProperty CertSubject)" + } + choco config set centralManagementServiceUrl "$($CcmEndpoint):24020/ChocolateyManagementService" + + # Updating the Registration Script + $EndpointScript = "$PSScriptRoot\scripts\Register-C4bEndpoint.ps1" + Invoke-TextReplacementInFile -Path $EndpointScript -Replacement @{ + "{{ ClientSaltValue }}" = Get-ChocoEnvironmentProperty ClientSalt -AsPlainText + "{{ ServiceSaltValue }}" = Get-ChocoEnvironmentProperty ServiceSalt -AsPlainText + "{{ FQDN }}" = Get-ChocoEnvironmentProperty CertSubject + + # Set a default value for TrustCertificate if we're using a self-signed cert + '(?\s+\$TrustCertificate)(?\s*=\s*\$true)?(?,)?(?!\))' = "`${Parameter}$( + if (Test-SelfSignedCertificate -Certificate $MyCertificate) {' = $true'} + )`${Comma}" + } + + # Create the site hosting the certificate import script on port 80 + if ($MyCertificate.NotAfter -gt (Get-Date).AddYears(5)) { + .\scripts\New-IISCertificateHost.ps1 + } + + Wait-Site CCM + + Write-Host "Configuring Chocolatey Central Management" + + # Run initial configuration for CCM Admin + if (-not ($CCMCredential = Get-ChocoEnvironmentProperty CCMCredential)) { + $CCMCredential = [PSCredential]::new( + "ccmadmin", + (New-ServicePassword) + ) + Set-CcmAccountPassword -CcmEndpoint $CcmEndpoint -ConnectionString "Server=Localhost\SQLEXPRESS;Database=ChocolateyManagement;User ID=$DatabaseUser;Password=$DatabaseUserPw;" -NewPassword $CCMCredential.Password + Set-ChocoEnvironmentProperty CCMCredential $CCMCredential + } + + if (-not ($CCMEncryptionPassword = Get-ChocoEnvironmentProperty CCMEncryptionPassword)) { + $CCMEncryptionPassword = New-ServicePassword + + Set-CcmEncryptionPassword -CcmEndpoint $CcmEndpoint -Credential $CCMCredential -NewPassword $CCMEncryptionPassword + Set-ChocoEnvironmentProperty CCMEncryptionPassword $CCMEncryptionPassword + } + + # Set Client and Service salts + if (-not (Get-ChocoEnvironmentProperty ClientSalt)) { + $ClientSaltValue = New-ServicePassword + Set-ChocoEnvironmentProperty ClientSalt $ClientSaltValue + + Invoke-Choco config set centralManagementClientCommunicationSaltAdditivePassword $ClientSaltValue.ToPlainText() + } + + if (-not (Get-ChocoEnvironmentProperty ServiceSalt)) { + $ServiceSaltValue = New-ServicePassword + Set-ChocoEnvironmentProperty ServiceSalt $ServiceSaltValue + + Invoke-Choco config set centralManagementServiceCommunicationSaltAdditivePassword $ServiceSaltValue.ToPlainText() + } + + # Set Website Root Address + Update-CcmSettings -CcmEndpoint $CCmEndpoint -Credential $CCMCredential -Settings @{ + website = @{ + webSiteRootAddress = $CcmEndpoint + } + } + $CcmSvcUrl = Invoke-Choco config get centralManagementServiceUrl -r Update-Clixml -Properties @{ CCMServiceURL = $CcmSvcUrl - CCMWebPortal = "http://localhost/Account/Login" - DefaultUser = "ccmadmin" - DefaultPwToBeChanged = "123qwe" + CCMWebPortal = "$CcmEndpoint/Account/Login" CCMDBUser = $DatabaseUser CCMInstallUser = whoami } diff --git a/Start-C4bJenkinsSetup.ps1 b/Start-C4bJenkinsSetup.ps1 index 0a52c4c..0f5a668 100644 --- a/Start-C4bJenkinsSetup.ps1 +++ b/Start-C4bJenkinsSetup.ps1 @@ -18,6 +18,31 @@ param( [string]$NuGetApiKey = $( if (-not (Get-Command Get-ChocoEnvironmentProperty -ErrorAction SilentlyContinue)) {. $PSScriptRoot\scripts\Get-Helpers.ps1} Get-ChocoEnvironmentProperty NuGetApiKey -AsPlainText + ), + + # The certificate thumbprint that identifies the target SSL certificate in + # the local machine certificate stores. + [Parameter(ValueFromPipeline, ParameterSetName='Thumbprint')] + [ArgumentCompleter({ + Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { + [System.Management.Automation.CompletionResult]::new( + $_.Thumbprint, + $_.Thumbprint, + "ParameterValue", + ($_.Subject -replace "^CN=(?.+),?.*$",'${FQDN}') + ) + } + })] + [ValidateScript({Test-CertificateDomain -Thumbprint $_})] + [string] + $Thumbprint = $( + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint + } else { + Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { + $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed + } | Select-Object -ExpandProperty Thumbprint -First 1 + } ) ) process { @@ -25,6 +50,8 @@ process { $ErrorActionPreference = 'Stop' Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Start-C4bJenkinsSetup-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" + $JenkinsScheme, $JenkinsPort = "http", "8080" + # Install temurin21jre to meet JRE>11 dependency of Jenkins $chocoArgs = @('install', 'temurin21jre', "--source='ChocolateyInternal'", '-y', '--no-progress', "--params='/ADDLOCAL=FeatureJavaHome'") & Invoke-Choco @chocoArgs @@ -37,8 +64,8 @@ process { $chocoArgs = @('install', 'jenkins', "--source='ChocolateyInternal'", '-y', '--no-progress') & Invoke-Choco @chocoArgs - Write-Host "Giving Jenkins 30 seconds to complete background setup..." -ForegroundColor Green - Start-Sleep -Seconds 30 # Jenkins needs a moment + # Jenkins needs a moment + Wait-Site Jenkins # Disabling inital setup prompts $JenkinsHome = "C:\ProgramData\Jenkins\.jenkins" @@ -47,25 +74,26 @@ process { $JenkinsVersion | Out-File -FilePath $JenkinsHome\jenkins.install.UpgradeWizard.state -Encoding utf8 $JenkinsVersion | Out-File -FilePath $JenkinsHome\jenkins.install.InstallUtil.lastExecVersion -Encoding utf8 - # Set the hostname, such that it's ready for use. - Set-JenkinsLocationConfiguration -Url "http://$($HostName):8080" -Path $JenkinsHome\jenkins.model.JenkinsLocationConfiguration.xml - #region Set Jenkins Password $JenkinsCred = Set-JenkinsPassword -UserName 'admin' -NewPassword $(New-ServicePassword) -PassThru #endregion - # Long winded way to get the scripts for Jenkins jobs into the right place, but easier to maintain going forward - $root = Split-Path -Parent $MyInvocation.MyCommand.Definition - $systemRoot = $env:SystemDrive + '\' - $JenkinsRoot = Join-Path $root -ChildPath 'jenkins' - $jenkinsScripts = Join-Path $JenkinsRoot -ChildPath 'scripts' + Stop-Service -Name Jenkins + + if ($Thumbprint) { + $JenkinsScheme, $JenkinsPort = "https", 7443 - #Set home directory of Jenkins install - $JenkinsHome = 'C:\ProgramData\Jenkins\.jenkins' + if ($SubjectWithoutCn = Get-ChocoEnvironmentProperty CertSubject) { + $Hostname = $SubjectWithoutCn + } - Copy-Item $jenkinsScripts $systemRoot -Recurse -Force + # Generate Jenkins keystore + Set-JenkinsCertificate -Thumbprint $Thumbprint -Port $JenkinsPort - Stop-Service -Name Jenkins + # Add firewall rule for Jenkins + netsh advfirewall firewall add rule name="Jenkins-$($JenkinsPort)" dir=in action=allow protocol=tcp localport=$JenkinsPort + } + Set-JenkinsLocationConfiguration -Url "$($JenkinsScheme)://$($SubjectWithoutCn):$($JenkinsPort)" -Path $JenkinsHome\jenkins.model.JenkinsLocationConfiguration.xml #region Jenkins Plugin Install & Update $JenkinsPlugins = (Get-Content $PSScriptRoot\files\jenkins.json | ConvertFrom-Json).plugins @@ -103,13 +131,8 @@ process { #endregion #region Job Config - Write-Host "Creating Chocolatey Jobs" -ForegroundColor Green - Get-ChildItem "$env:SystemDrive\choco-setup\files\jenkins" | Copy-Item -Destination "$JenkinsHome\jobs\" -Recurse -Force - - Get-ChildItem -Path "$JenkinsHome\jobs" -Recurse -File -Filter 'config.xml' | Invoke-TextReplacementInFile -Replacement @{ - '{{NugetApiKey}}' = $NuGetApiKey - '(?<=https:\/\/)(?.+)(?=:8443\/repository\/)' = $HostName - } + $chocoArgs = @('install', 'chocolatey-licensed-jenkins-jobs', "--source='ChocolateyInternal'", '-y', '--no-progress') + & Invoke-Choco @chocoArgs #endregion Write-Host "Starting Jenkins service back up" -ForegroundColor Green @@ -117,7 +140,7 @@ process { # Save useful params Update-Clixml -Properties @{ - JenkinsUri = "http://$($HostName):8080" + JenkinsUri = "$($JenkinsScheme)://$($HostName):$($JenkinsPort)" JenkinsCredential = $JenkinsCred } diff --git a/Start-C4bNexusSetup.ps1 b/Start-C4bNexusSetup.ps1 index a8249d0..910a23f 100644 --- a/Start-C4bNexusSetup.ps1 +++ b/Start-C4bNexusSetup.ps1 @@ -15,16 +15,37 @@ C4B Quick-Start Guide Nexus setup script - Setup of firewall rule for repository access #> [CmdletBinding()] -param( - # Choice of non-IE broswer for Nexus +param( + # The certificate thumbprint that identifies the target SSL certificate in + # the local machine certificate stores. [Parameter()] + [ArgumentCompleter({ + Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { + [System.Management.Automation.CompletionResult]::new( + $_.Thumbprint, + $_.Thumbprint, + "ParameterValue", + ($_.Subject -replace "^CN=(?.+),?.*$",'${FQDN}') + ) + } + })] + [ValidateScript({Test-CertificateDomain -Thumbprint $_})] [string] - $Browser = 'Edge' + $Thumbprint = $( + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint + } else { + Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { + $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed + } | Select-Object -ExpandProperty Thumbprint -First 1 + } + ) ) process { $DefaultEap = $ErrorActionPreference $ErrorActionPreference = 'Stop' Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Start-C4bNexusSetup-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" + $NexusPort = 8081 $Packages = (Get-Content $PSScriptRoot\files\chocolatey.json | ConvertFrom-Json).packages @@ -33,34 +54,169 @@ process { $chocoArgs = @('install', 'nexus-repository', '-y' ,'--no-progress', "--package-parameters='/Fqdn:localhost'") & Invoke-Choco @chocoArgs - #Build Credential Object, Connect to Nexus + $chocoArgs = @('install', 'nexushell', '-y' ,'--no-progress') + & Invoke-Choco @chocoArgs + + if ($Thumbprint) { + $NexusPort = 8443 + + $null = Set-NexusCert -Thumbprint $Thumbprint -Port $NexusPort + + if ($CertificateDnsName = Get-ChocoEnvironmentProperty CertSubject) { + # Override the domain, so we don't get prompted for wildcard certificates + Get-NexusLocalServiceUri -HostnameOverride $CertificateDnsName | Write-Verbose + } + } + + # Add Nexus port access via firewall + $FwRuleParams = @{ + DisplayName = "Nexus Repository access on TCP $NexusPort" + Direction = 'Inbound' + LocalPort = $NexusPort + Protocol = 'TCP' + Action = 'Allow' + } + $null = New-NetFirewallRule @FwRuleParams + + Wait-Site Nexus + Write-Host "Configuring Sonatype Nexus Repository" - $securePw = (Get-Content 'C:\programdata\sonatype-work\nexus3\admin.password') | ConvertTo-SecureString -AsPlainText -Force - $Credential = [System.Management.Automation.PSCredential]::new('admin',$securePw) - Connect-NexusServer -Hostname localhost -Credential $Credential + # Build Credential Object, Connect to Nexus + if (-not ($Credential = Get-ChocoEnvironmentProperty NexusCredential)) { + Write-Host "Setting up admin account." + $NexusDefaultPasswordPath = 'C:\programdata\sonatype-work\nexus3\admin.password' + + $Timeout = [System.Diagnostics.Stopwatch]::StartNew() + while (-not (Test-Path $NexusDefaultPasswordPath) -and $Timeout.Elapsed.TotalMinutes -lt 3) { + Start-Sleep -Seconds 5 + } - #Drain default repositories + $DefaultNexusCredential = [System.Management.Automation.PSCredential]::new( + 'admin', + (Get-Content $NexusDefaultPasswordPath | ConvertTo-SecureString -AsPlainText -Force) + ) + + try { + Connect-NexusServer -LocalService -Credential $DefaultNexusCredential -ErrorAction Stop + + $Credential = [PSCredential]::new( + "admin", + (New-ServicePassword) + ) + + Set-NexusUserPassword -Username admin -NewPassword $Credential.Password -ErrorAction Stop + Set-ChocoEnvironmentProperty -Name NexusCredential -Value $Credential + } finally {} + + if (Test-Path $NexusDefaultPasswordPath) { + Remove-Item -Path $NexusDefaultPasswordPath + } + } + Connect-NexusServer -LocalService -Credential $Credential + + # Disable anonymous authentication + $null = Set-NexusAnonymousAuth -Disabled + + # Drain default repositories $null = Get-NexusRepository | Where-Object Name -NotLike "choco*" | Remove-NexusRepository -Force - #Enable NuGet Auth Realm + # Enable NuGet Auth Realm Enable-NexusRealm -Realm 'NuGet API-Key Realm' - #Create Chocolatey repositories - New-NexusNugetHostedRepository -Name ChocolateyInternal -DeploymentPolicy Allow - New-NexusNugetHostedRepository -Name ChocolateyTest -DeploymentPolicy Allow - New-NexusRawHostedRepository -Name choco-install -DeploymentPolicy Allow -ContentDisposition Attachment + # Create Chocolatey repositories + if (-not (Get-NexusRepository -Name ChocolateyInternal)) { + New-NexusNugetHostedRepository -Name ChocolateyInternal -DeploymentPolicy Allow + } - #Surface API Key - $NuGetApiKey = (Get-NexusNuGetApiKey -Credential $Credential).apikey + if (-not (Get-NexusRepository -Name ChocolateyTest)) { + New-NexusNugetHostedRepository -Name ChocolateyTest -DeploymentPolicy Allow + } - # Push all packages from previous steps to NuGet repo - Get-ChildItem -Path "$env:SystemDrive\choco-setup\files\files" -Filter *.nupkg | ForEach-Object { - Invoke-Choco push $_.FullName --source "$((Get-NexusRepository -Name 'ChocolateyInternal').url)/index.json" --apikey $NugetApiKey --force + if (-not (Get-NexusRepository -Name choco-install)) { + New-NexusRawHostedRepository -Name choco-install -DeploymentPolicy Allow -ContentDisposition Attachment } - # Temporary workaround to reset the NuGet v3 cache, such that it doesn't capture localhost as the FQDN - Remove-NexusRepositoryFolder -RepositoryName ChocolateyInternal -Name v3 + # Create role for end user to pull from Nexus + if (-not ($NexusRole = Get-NexusRole -Role 'chocorole' -ErrorAction SilentlyContinue)) { + # Create Nexus role + $RoleParams = @{ + Id = "chocorole" + Name = "chocorole" + Description = "Role for web enabled choco clients" + Privileges = @('nx-repository-view-nuget-*-browse', 'nx-repository-view-nuget-*-read', 'nx-repository-view-raw-*-read', 'nx-repository-view-raw-*-browse') + } + $NexusRole = New-NexusRole @RoleParams + + $Timeout = [System.Diagnostics.Stopwatch]::StartNew() + while ($Timeout.Elapsed.TotalSeconds -lt 30 -and -not (Get-NexusRole -Role $RoleParams.Id -ErrorAction SilentlyContinue)) { + Start-Sleep -Seconds 3 + } + } + + # Create new user for endpoints + if (-not (Get-NexusUser -User 'chocouser' -ErrorAction SilentlyContinue)) { + # Create Nexus user + $UserParams = @{ + Username = 'chocouser' + Password = New-ServicePassword + FirstName = 'Choco' + LastName = 'User' + EmailAddress = 'chocouser@example.com' + Status = 'Active' + Roles = $NexusRole.Id + } + $null = New-NexusUser @UserParams + + $Timeout = [System.Diagnostics.Stopwatch]::StartNew() + while ($Timeout.Elapsed.TotalSeconds -lt 30 -and -not (Get-NexusUser -User $UserParams.Username -ErrorAction SilentlyContinue)) { + Start-Sleep -Seconds 3 + } + + Set-ChocoEnvironmentProperty ChocoUserPassword $UserParams.Password + } + + # Create role for task runner to push to Nexus + if (-not ($PackageUploadRole = Get-NexusRole -Role "package-uploader" -ErrorAction SilentlyContinue)) { + $PackageUploadRole = New-NexusRole -Name "package-uploader" -Id "package-uploader" -Description "Role allowed to push and list packages" -Privileges @( + "nx-repository-view-nuget-*-edit" + "nx-repository-view-nuget-*-read" + "nx-apikey-all" + ) + } + + # Create new user for package-upload - as this changes the usercontext, ensure this is the last thing in the script, or it's in a job + if ($UploadUser = Get-ChocoEnvironmentProperty PackageUploadCredential) { + Write-Verbose "Using existing PackageUpload credential '$($UploadUser.UserName)'" + } else { + $UploadUser = [PSCredential]::new( + 'chocoPackager', + (New-ServicePassword -Length 64) + ) + } + + if (-not (Get-NexusUser -User $UploadUser.UserName)) { + $NewUser = @{ + Username = $UploadUser.UserName + Password = $UploadUser.Password + FirstName = "Chocolatey" + LastName = "Packager" + EmailAddress = "packager@$env:ComputerName.local" + Status = "Active" + Roles = $PackageUploadRole.Id + } + $null = New-NexusUser @NewUser + + Set-ChocoEnvironmentProperty -Name PackageUploadCredential -Value $UploadUser + } + + # Retrieve the API Key to use in Jenkins et al + if ($NuGetApiKey = Get-ChocoEnvironmentProperty PackageApiKey) { + Write-Verbose "Using existing Nexus Api Key for '$($UploadUser.UserName)'" + } else { + $NuGetApiKey = (Get-NexusNuGetApiKey -Credential $UploadUser).apiKey + Set-ChocoEnvironmentProperty -Name PackageApiKey -Value $NuGetApiKey + } # Push latest ChocolateyInstall.ps1 to raw repo $ScriptDir = "$env:SystemDrive\choco-setup\files\scripts" @@ -73,21 +229,36 @@ process { $Signature = Get-AuthenticodeSignature -FilePath $ChocoInstallScript if ($Signature.Status -eq 'Valid' -and $Signature.SignerCertificate.Subject -eq 'CN="Chocolatey Software, Inc", O="Chocolatey Software, Inc", L=Topeka, S=Kansas, C=US') { - New-NexusRawComponent -RepositoryName 'choco-install' -File $ChocoInstallScript + $null = New-NexusRawComponent -RepositoryName 'choco-install' -File $ChocoInstallScript } else { Write-Error "ChocolateyInstall.ps1 script signature is not valid. Please investigate." } + # Push ClientSetup.ps1 to raw repo + $ClientScript = "$PSScriptRoot\scripts\ClientSetup.ps1" + (Get-Content -Path $ClientScript) -replace "{{hostname}}", "$((Get-NexusLocalServiceUri) -replace '^https?:\/\/')" | Set-Content -Path ($TemporaryFile = New-TemporaryFile).FullName + $null = New-NexusRawComponent -RepositoryName 'choco-install' -File $TemporaryFile.FullName -Name "ClientSetup.ps1" + # Nexus NuGet V3 Compatibility Invoke-Choco feature disable --name="'usePackageRepositoryOptimizations'" # Add ChocolateyInternal as a source repository - Invoke-Choco source add -n 'ChocolateyInternal' -s "$((Get-NexusRepository -Name 'ChocolateyInternal').url)/index.json" --priority 1 + $LocalSource = "$((Get-NexusRepository -Name 'ChocolateyInternal').url)/index.json" + Invoke-Choco source add -n 'ChocolateyInternal' -s $LocalSource -u="$($UploadUser.UserName)" -p="$($UploadUser.GetNetworkCredential().Password)" --priority 1 # Add ChocolateyTest as a source repository, to enable authenticated pushing - Invoke-Choco source add -n 'ChocolateyTest' -s "$((Get-NexusRepository -Name 'ChocolateyTest').url)/index.json" + Invoke-Choco source add -n 'ChocolateyTest' -s "$((Get-NexusRepository -Name 'ChocolateyTest').url)/index.json" -u="$($UploadUser.UserName)" -p="$($UploadUser.GetNetworkCredential().Password)" Invoke-Choco source disable -n 'ChocolateyTest' + # Push all packages from previous steps to NuGet repo + Write-Host "Pushing C4B Environment Packages to ChocolateyInternal" + Get-ChildItem -Path "$env:SystemDrive\choco-setup\files\files" -Filter *.nupkg | ForEach-Object { + Invoke-Choco push $_.FullName --source $LocalSource --apikey $NugetApiKey --force + } + + # Temporary workaround to reset the NuGet v3 cache, such that it doesn't capture localhost as the FQDN + Remove-NexusRepositoryFolder -RepositoryName ChocolateyInternal -Name v3 + # Remove Local Chocolatey Setup Source $chocoArgs = @('source', 'remove', '--name="LocalChocolateySetup"') & Invoke-Choco @chocoArgs @@ -96,33 +267,16 @@ process { if (-not (Test-Path 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe')) { Write-Host "Installing Microsoft Edge, to allow viewing the Nexus site" Invoke-Choco install microsoft-edge -y --source ChocolateyInternal - if ($LASTEXITCODE -eq 0) { - if (Test-Path 'HKLM:\SOFTWARE\Microsoft\Edge') { - $RegArgs = @{ - Path = 'HKLM:\SOFTWARE\Microsoft\Edge\' - Name = 'HideFirstRunExperience' - Type = 'Dword' - Value = 1 - Force = $true - } - $null = Set-ItemProperty @RegArgs - } - } } - - # Add Nexus port 8081 access via firewall - $FwRuleParams = @{ - DisplayName = 'Nexus Repository access on TCP 8081' - Direction = 'Inbound' - LocalPort = 8081 - Protocol = 'TCP' - Action = 'Allow' + $Key = @{ Key = 'HKLM:\Software\Policies\Microsoft\Edge' ; Value = 'HideFirstRunExperience' } + if (-not (Test-Path $Key.Key)) { + $null = New-Item -Path $Key.Key -Force } - $null = New-NetFirewallRule @FwRuleParams + $null = New-ItemProperty -Path $Key.Key -Name $Key.Value -Value 1 -PropertyType DWORD -Force # Save useful params Update-Clixml -Properties @{ - NexusUri = "http://localhost:8081" + NexusUri = Get-NexusLocalServiceUri NexusCredential = $Credential NexusRepo = "$((Get-NexusRepository -Name 'ChocolateyInternal').url)/index.json" NuGetApiKey = $NugetApiKey | ConvertTo-SecureString -AsPlainText -Force diff --git a/Switch-SslSecurity.ps1 b/Switch-SslSecurity.ps1 new file mode 100644 index 0000000..860474c --- /dev/null +++ b/Switch-SslSecurity.ps1 @@ -0,0 +1,220 @@ +#requires -modules C4B-Environment +using namespace System.Net.Sockets +using namespace System.Net.Security +using namespace System.Security.Cryptography.X509Certificates +<# +.SYNOPSIS +Generates or retrieves certificates and generates SSL bindings for +Central Management and Nexus. + +.DESCRIPTION +Removes any existing certificates which have the subject "chocoserver" to avoid +issues, and then either generates or retrieves the required certificate. The +certificate is placed in the required local machine store, and then the script +generates SSL bindings for both Nexus and the Central Management website using the +certificate. +#> +[CmdletBinding(DefaultParameterSetName='SelfSigned')] +[OutputType([string])] +param( + # The certificate thumbprint that identifies the target SSL certificate in + # the local machine certificate stores. + # Ignored if supplied alongside -Subject. + [Parameter(ValueFromPipeline, ParameterSetName='Thumbprint')] + [ArgumentCompleter({ + Get-ChildItem Cert:\LocalMachine\TrustedPeople | ForEach-Object { + [System.Management.Automation.CompletionResult]::new( + $_.Thumbprint, + $_.Thumbprint, + "ParameterValue", + ($_.Subject -replace "^CN=(?.+),?.*$",'${FQDN}') + ) + } + })] + [ValidateScript({Test-CertificateDomain -Thumbprint $_})] + [string] + $Thumbprint = $( + if ((Test-Path C:\choco-setup\clixml\chocolatey-for-business.xml) -and (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint) { + (Import-Clixml C:\choco-setup\clixml\chocolatey-for-business.xml).CertThumbprint + } else { + Get-ChildItem Cert:\LocalMachine\TrustedPeople -Recurse | Sort-Object { + $_.Issuer -eq $_.Subject # Prioritise any certificates above self-signed + } | Select-Object -ExpandProperty Thumbprint -First 1 + } + ), + + # The certificate subject that identifies the target SSL certificate in + # the local machine certificate stores. + [Parameter(ParameterSetName='Subject')] + [string] + $Subject, + + # If using a wildcard certificate, provide a DNS name you want to use to access services secured by the certificate. + [Parameter(ParameterSetName='Subject')] + [Parameter(ParameterSetName='Thumbprint')] + [string] + $CertificateDnsName = $( + if (-not (Get-Command Get-ChocoEnvironmentProperty -ErrorAction SilentlyContinue)) {. $PSScriptRoot\scripts\Get-Helpers.ps1} + Get-ChocoEnvironmentProperty CertSubject + ), + + # API key of your Nexus repo, to add to the source setup on C4B Server. + [string]$NuGetApiKey = $( + if (-not (Get-Command Get-ChocoEnvironmentProperty -ErrorAction SilentlyContinue)) {. $PSScriptRoot\scripts\Get-Helpers.ps1} + Get-ChocoEnvironmentProperty NuGetApiKey -AsPlainText + ) +) +process { + $DefaultEap = $ErrorActionPreference + $ErrorActionPreference = 'Stop' + Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Switch-SslSecurity-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" + + # Collect current certificate configuration + $Certificate = if ($Subject) { + Get-Certificate -Subject $Subject + } elseif ($Thumbprint) { + Get-Certificate -Thumbprint $Thumbprint + } + + if (-not $CertificateDnsName -and -not ($CertificateDnsName = Get-ChocoEnvironmentProperty CertSubject)) { + $null = Test-CertificateDomain -Thumbprint $Certificate.Thumbprint + } else { + $SubjectWithoutCn = $CertificateDnsName + } + + <# Nexus #> + # Stop Services/Processes/Websites required + Stop-Service nexus + + # Put certificate in TrustedPeople + Copy-CertToStore -Certificate $Certificate + + # Generate Nexus keystore + $null = Set-NexusCert -Thumbprint $Certificate.Thumbprint + + # Add firewall rule for Nexus + netsh advfirewall firewall add rule name="Nexus-8443" dir=in action=allow protocol=tcp localport=8443 + + Write-Verbose "Starting up Nexus" + Start-Service nexus + + Write-Warning "Waiting to give Nexus time to start up on 'https://${SubjectWithoutCn}:8443'" + Wait-Site Nexus + + # Build Credential Object, Connect to Nexus + if ($Credential = Get-ChocoEnvironmentProperty NexusCredential) { + Write-Verbose "Using stored Nexus Credential" + } elseif (Test-Path 'C:\programdata\sonatype-work\nexus3\admin.password') { + $securePw = (Get-Content 'C:\programdata\sonatype-work\nexus3\admin.password') | ConvertTo-SecureString -AsPlainText -Force + $Credential = [System.Management.Automation.PSCredential]::new('admin', $securePw) + } + + # Connect to Nexus + Connect-NexusServer -Hostname $SubjectWithoutCn -Credential $Credential -UseSSL + + # Push ClientSetup.ps1 to raw repo + $ClientScript = "$PSScriptRoot\scripts\ClientSetup.ps1" + (Get-Content -Path $ClientScript) -replace "{{hostname}}", "$((Get-NexusLocalServiceUri) -replace '^https?:\/\/')" | Set-Content -Path ($TemporaryFile = New-TemporaryFile).FullName + $null = New-NexusRawComponent -RepositoryName 'choco-install' -File $TemporaryFile.FullName -Name "ClientSetup.ps1" + + $NexusPw = Get-ChocoEnvironmentProperty ChocoUserPassword -AsPlainText + + # Update all sources with credentials and the new path + foreach ($Repository in Get-NexusRepository -Format nuget | Where-Object Type -eq 'hosted') { + $RepositoryUrl = "https://${SubjectWithoutCn}:8443/repository/$($Repository.Name)/index.json" + + $ChocoArgs = @( + 'source', + 'add', + "--name='$($Repository.Name)'", + "--source='$RepositoryUrl'", + '--priority=1', + "--user='chocouser'", + "--password='$NexusPw'" + ) + & Invoke-Choco @ChocoArgs + + # Update Repository API key + $chocoArgs = @('apikey', "--source='$RepositoryUrl'", "--api-key='$NuGetApiKey'") + & Invoke-Choco @chocoArgs + + # Reset the NuGet v3 cache, such that it doesn't capture localhost as the FQDN + Remove-NexusRepositoryFolder -RepositoryName $Repository.Name -Name v3 + } + + Update-Clixml -Properties @{ + NexusUri = "https://$($SubjectWithoutCn):8443" + NexusRepo = "https://${SubjectWithoutCn}:8443/repository/ChocolateyInternal/index.json" + } + + <# Jenkins #> + $JenkinsHome = "C:\ProgramData\Jenkins\.jenkins" + $JenkinsPort = 7443 + + Set-JenkinsLocationConfiguration -Url "https://$($SubjectWithoutCn):$($JenkinsPort)" -Path $JenkinsHome\jenkins.model.JenkinsLocationConfiguration.xml + + # Generate Jenkins keystore + Set-JenkinsCertificate -Thumbprint $Certificate.Thumbprint -Port $JenkinsPort + + # Add firewall rule for Jenkins + netsh advfirewall firewall add rule name="Jenkins-$($JenkinsPort)" dir=in action=allow protocol=tcp localport=$JenkinsPort + + # Update job parameters in Jenkins + $NexusUri = Get-ChocoEnvironmentProperty NexusUri + Update-JenkinsJobParameters -Replacement @{ + "P_DST_URL" = "$NexusUri/repository/ChocolateyTest/index.json" + "P_LOCAL_REPO_URL" = "$NexusUri/repository/ChocolateyTest/index.json" + "P_TEST_REPO_URL" = "$NexusUri/repository/ChocolateyTest/index.json" + "P_PROD_REPO_URL" = "$NexusUri/repository/ChocolateyInternal/index.json" + } + + Update-Clixml -Properties @{ + JenkinsUri = "https://$($SubjectWithoutCn):$($JenkinsPort)" + } + + <# CCM #> + # Update the service certificate + Set-CcmCertificate -CertificateThumbprint $Certificate.Thumbprint + + # Remove old CCM web binding, and add new CCM web binding + Stop-CcmService + Remove-CcmBinding + New-CcmBinding -Thumbprint $Certificate.Thumbprint + Start-CcmService + + # Create the site hosting the certificate import script on port 80 + # Only run this if it's a self-signed cert which has 10-year validity + if ($Certificate.NotAfter -gt (Get-Date).AddYears(5)) { + $IsSelfSigned = $true + .\scripts\New-IISCertificateHost.ps1 + } + + # Generate Register-C4bEndpoint.ps1 + $EndpointScript = "$PSScriptRoot\scripts\Register-C4bEndpoint.ps1" + + Invoke-TextReplacementInFile -Path $EndpointScript -Replacement @{ + "{{ ClientSaltValue }}" = Get-ChocoEnvironmentProperty ClientSalt -AsPlainText + "{{ ServiceSaltValue }}" = Get-ChocoEnvironmentProperty ServiceSalt -AsPlainText + "{{ FQDN }}" = $SubjectWithoutCn + + # Set a default value for TrustCertificate if we're using a self-signed cert + '(?\s+\$TrustCertificate)(?\s*=\s*\$true)?(?,)?(?!\))' = "`${Parameter}$( + if (Test-SelfSignedCertificate -Certificate $Certificate) {' = $true'} + )`${Comma}" + } + + Update-Clixml -Properties @{ + CCMWebPortal = "https://$($SubjectWithoutCn)/Account/Login" + CCMServiceURL = "https://$($SubjectWithoutCn):24020/ChocolateyManagementService" + CertSubject = $SubjectWithoutCn + CertThumbprint = $Certificate.Thumbprint + CertExpiry = $Certificate.NotAfter + IsSelfSigned = $IsSelfSigned + } +} +end { + $ErrorActionPreference = $DefaultEap + Stop-Transcript + + Complete-C4bSetup -SkipBrowserLaunch +} \ No newline at end of file diff --git a/Start-C4bVerification.ps1 b/Test-C4bSetup.ps1 similarity index 95% rename from Start-C4bVerification.ps1 rename to Test-C4bSetup.ps1 index 639c4f6..c4a5de1 100644 --- a/Start-C4bVerification.ps1 +++ b/Test-C4bSetup.ps1 @@ -1,9 +1,9 @@ #requires -modules C4B-Environment [CmdletBinding()] Param( - [Parameter(Mandatory)] + [Parameter()] [String] - $Fqdn + $Fqdn = $(Get-ChocoEnvironmentProperty CertSubject) ) process { if (-not (Get-Module Pester -ListAvailable).Where{$_.Version -gt "5.0"}) { diff --git a/files/chocolatey.json b/files/chocolatey.json index ebf4f14..34e1130 100644 --- a/files/chocolatey.json +++ b/files/chocolatey.json @@ -4,6 +4,8 @@ { "name": "chocolatey-compatibility.extension" }, { "name": "chocolatey-core.extension" }, { "name": "chocolatey-dotnetfx.extension" }, + { "name": "chocolatey-licensed-jenkins-jobs" }, + { "name": "chocolatey-licensed-jenkins-scripts" }, { "name": "chocolatey-management-database", "internalize": false }, { "name": "chocolatey-management-service", "internalize": false }, { "name": "chocolatey-management-web", "internalize": false }, @@ -12,9 +14,9 @@ { "name": "chocolatey" }, { "name": "chocolateygui.extension" }, { "name": "chocolateygui" }, - { "name": "dotnet-8.0-aspnetruntime", "version": "8.0.8" }, - { "name": "dotnet-8.0-runtime", "version": "8.0.8" }, - { "name": "dotnet-aspnetcoremodule-v2", "version": "18.0.24201" }, + { "name": "dotnet-8.0-aspnetruntime", "version": "8.0.16" }, + { "name": "dotnet-8.0-runtime", "version": "8.0.16" }, + { "name": "dotnet-aspnetcoremodule-v2", "version": "18.0.25074" }, { "name": "dotnetfx" }, { "name": "jenkins" }, { "name": "KB2919355", "internalize": false }, @@ -23,6 +25,7 @@ { "name": "KB3033929", "internalize": false }, { "name": "KB3035131", "internalize": false }, { "name": "microsoft-edge" }, + { "name": "nexushell", "version": "1.2.0" }, { "name": "nexus-repository" }, { "name": "pester", "internalize": false }, { "name": "sql-server-express" }, diff --git a/files/jenkins.json b/files/jenkins.json index da5e171..ba32c66 100644 --- a/files/jenkins.json +++ b/files/jenkins.json @@ -1,33 +1,33 @@ { "plugins": [ - { "name": "apache-httpcomponents-client-4-api", "version": "4.5.14-208.v438351942757" }, - { "name": "asm-api", "version": "9.7-33.v4d23ef79fcc8" }, - { "name": "bouncycastle-api", "version": "2.30.1.78.1-233.vfdcdeb_0a_08a_a_" }, - { "name": "branch-api", "version": "2.1169.va_f810c56e895" }, - { "name": "caffeine-api", "version": "3.1.8-133.v17b_1ff2e0599" }, - { "name": "cloudbees-folder", "version": "6.928.v7c780211d66e" }, - { "name": "display-url-api", "version": "2.204.vf6fddd8a_8b_e9" }, - { "name": "durable-task", "version": "555.v6802fe0f0b_82" }, - { "name": "instance-identity", "version": "185.v303dc7c645f9" }, - { "name": "ionicons-api", "version": "74.v93d5eb_813d5f" }, - { "name": "jakarta-activation-api", "version": "2.1.3-1" }, - { "name": "jakarta-mail-api", "version": "2.1.3-1" }, - { "name": "javax-activation-api", "version": "1.2.0-7" }, - { "name": "javax-mail-api", "version": "1.6.2-10" }, - { "name": "mailer", "version": "472.vf7c289a_4b_420" }, - { "name": "pipeline-groovy-lib", "version": "710.v4b_94b_077a_808" }, - { "name": "scm-api", "version": "690.vfc8b_54395023" }, - { "name": "script-security", "version": "1341.va_2819b_414686" }, - { "name": "structs", "version": "337.v1b_04ea_4df7c8" }, - { "name": "variant", "version": "60.v7290fc0eb_b_cd" }, - { "name": "workflow-api", "version": "1316.v33eb_726c50b_a_" }, - { "name": "workflow-basic-steps", "version": "1058.vcb_fc1e3a_21a_9" }, - { "name": "workflow-cps", "version": "3894.3896.vca_2c931e7935" }, - { "name": "workflow-durable-task-step", "version": "1353.v1891a_b_01da_18" }, - { "name": "workflow-job", "version": "1400.v7fd111b_ec82f" }, - { "name": "workflow-multibranch", "version": "783.va_6eb_ef636fb_d" }, - { "name": "workflow-scm-step", "version": "427.v4ca_6512e7df1" }, - { "name": "workflow-step-api", "version": "657.v03b_e8115821b_" }, - { "name": "workflow-support", "version": "907.v6713a_ed8a_573" } + { "name": "apache-httpcomponents-client-4-api", "version": "4.5.14-269.vfa_2321039a_83" }, + { "name": "asm-api", "version": "9.8-135.vb_2239d08ee90" }, + { "name": "bouncycastle-api", "version": "2.30.1.80-256.vf98926042a_9b_" }, + { "name": "branch-api", "version": "2.1217.v43d8b_b_d8b_2c7" }, + { "name": "caffeine-api", "version": "3.2.0-166.v72a_6d74b_870f" }, + { "name": "cloudbees-folder", "version": "6.1012.v79a_86a_1ea_c1f" }, + { "name": "display-url-api", "version": "2.209.v582ed814ff2f" }, + { "name": "durable-task", "version": "587.v84b_877235b_45" }, + { "name": "instance-identity", "version": "201.vd2a_b_5a_468a_a_6" }, + { "name": "ionicons-api", "version": "82.v0597178874e1" }, + { "name": "jakarta-activation-api", "version": "2.1.3-2" }, + { "name": "jakarta-mail-api", "version": "2.1.3-2" }, + { "name": "javax-activation-api", "version": "1.2.0-8" }, + { "name": "javax-mail-api", "version": "1.6.2-11" }, + { "name": "mailer", "version": "489.vd4b_25144138f" }, + { "name": "pipeline-groovy-lib", "version": "752.vdddedf804e72" }, + { "name": "scm-api", "version": "704.v3ce5c542825a_" }, + { "name": "script-security", "version": "1373.vb_b_4a_a_c26fa_00" }, + { "name": "structs", "version": "343.vdcf37b_a_c81d5" }, + { "name": "variant", "version": "70.va_d9f17f859e0" }, + { "name": "workflow-api", "version": "1371.ve334280b_d611" }, + { "name": "workflow-basic-steps", "version": "1079.vce64b_a_929c5a_" }, + { "name": "workflow-cps", "version": "4046.v90b_1b_9edec67" }, + { "name": "workflow-durable-task-step", "version": "1405.v1fcd4a_d00096" }, + { "name": "workflow-job", "version": "1520.v56d65e3b_4566" }, + { "name": "workflow-multibranch", "version": "806.vb_b_688f609ee9" }, + { "name": "workflow-scm-step", "version": "437.v05a_f66b_e5ef8" }, + { "name": "workflow-step-api", "version": "700.v6e45cb_a_5a_a_21" }, + { "name": "workflow-support", "version": "968.v8f17397e87b_8" } ] } \ No newline at end of file diff --git a/jenkins/Internalize packages from the Community Repository/builds/legacyIds b/jenkins/Internalize packages from the Community Repository/builds/legacyIds deleted file mode 100644 index e69de29..0000000 diff --git a/jenkins/Internalize packages from the Community Repository/builds/permalinks b/jenkins/Internalize packages from the Community Repository/builds/permalinks deleted file mode 100644 index d7703c1..0000000 --- a/jenkins/Internalize packages from the Community Repository/builds/permalinks +++ /dev/null @@ -1,2 +0,0 @@ -lastFailedBuild -1 -lastSuccessfulBuild -1 diff --git a/jenkins/Internalize packages from the Community Repository/config.xml b/jenkins/Internalize packages from the Community Repository/config.xml deleted file mode 100644 index c74691e..0000000 --- a/jenkins/Internalize packages from the Community Repository/config.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - Add new packages for internalizing from the Community Repository. - false - - - - - - P_PKG_LIST - List of Chocolatey packages to be internalized (comma separated). - - true - - - P_DST_URL - Internal package repository URL. - https://{{hostname}}:8443/repository/ChocolateyTest/index.json - true - - - P_API_KEY - API key for the internal test repository - {{NugetApiKey}} - - - - - - - true - - - false - diff --git a/jenkins/Internalize packages from the Community Repository/nextBuildNumber b/jenkins/Internalize packages from the Community Repository/nextBuildNumber deleted file mode 100644 index d00491f..0000000 --- a/jenkins/Internalize packages from the Community Repository/nextBuildNumber +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/jenkins/Update production repository/builds/legacyIds b/jenkins/Update production repository/builds/legacyIds deleted file mode 100644 index e69de29..0000000 diff --git a/jenkins/Update production repository/builds/permalinks b/jenkins/Update production repository/builds/permalinks deleted file mode 100644 index 3e5edba..0000000 --- a/jenkins/Update production repository/builds/permalinks +++ /dev/null @@ -1,6 +0,0 @@ -lastCompletedBuild -1 -lastFailedBuild -1 -lastStableBuild -1 -lastSuccessfulBuild -1 -lastUnstableBuild -1 -lastUnsuccessfulBuild -1 diff --git a/jenkins/Update production repository/config.xml b/jenkins/Update production repository/config.xml deleted file mode 100644 index a9d0f31..0000000 --- a/jenkins/Update production repository/config.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - Determine new packages on the Test repository, test and submit them to the Production repository. - false - - - - - - P_PROD_REPO_URL - URL to the production repository - https://{{hostname}}:8443/repository/ChocolateyInternal/index.json - true - - - P_PROD_REPO_API_KEY - API key for the production repository. - {{NugetApiKey}} - - - P_TEST_REPO_URL - URL for the test repository. - https://{{hostname}}:8443/repository/ChocolateyTest/index.json - true - - - - - - - - Internalize packages from the Community Repository, Update test repository from Chocolatey Community Repository, - - SUCCESS - 0 - BLUE - true - - - - - - - - true - - - false - diff --git a/jenkins/Update production repository/nextBuildNumber b/jenkins/Update production repository/nextBuildNumber deleted file mode 100644 index d00491f..0000000 --- a/jenkins/Update production repository/nextBuildNumber +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/jenkins/Update test repository from Chocolatey Community Repository/builds/legacyIds b/jenkins/Update test repository from Chocolatey Community Repository/builds/legacyIds deleted file mode 100644 index e69de29..0000000 diff --git a/jenkins/Update test repository from Chocolatey Community Repository/builds/permalinks b/jenkins/Update test repository from Chocolatey Community Repository/builds/permalinks deleted file mode 100644 index d7703c1..0000000 --- a/jenkins/Update test repository from Chocolatey Community Repository/builds/permalinks +++ /dev/null @@ -1,2 +0,0 @@ -lastFailedBuild -1 -lastSuccessfulBuild -1 diff --git a/jenkins/Update test repository from Chocolatey Community Repository/config.xml b/jenkins/Update test repository from Chocolatey Community Repository/config.xml deleted file mode 100644 index 6eeae59..0000000 --- a/jenkins/Update test repository from Chocolatey Community Repository/config.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - Automatically update any out of date packages in the test repository from the Community Repository - false - - - - - - P_LOCAL_REPO_URL - Internal test repository. - https://{{hostname}}:8443/repository/ChocolateyTest/index.json - true - - - P_REMOTE_REPO_URL - Remote repository containing updated package versions. - https://community.chocolatey.org/api/v2/ - true - - - P_LOCAL_REPO_API_KEY - API key for the internal test repository where updated packages will be pushed. - {{NugetApiKey}} - - - - - - - true - - - false - diff --git a/jenkins/Update test repository from Chocolatey Community Repository/nextBuildNumber b/jenkins/Update test repository from Chocolatey Community Repository/nextBuildNumber deleted file mode 100644 index d00491f..0000000 --- a/jenkins/Update test repository from Chocolatey Community Repository/nextBuildNumber +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/jenkins/scripts/ConvertTo-ChocoObject.ps1 b/jenkins/scripts/ConvertTo-ChocoObject.ps1 deleted file mode 100644 index 7702d9c..0000000 --- a/jenkins/scripts/ConvertTo-ChocoObject.ps1 +++ /dev/null @@ -1,16 +0,0 @@ -function ConvertTo-ChocoObject { - [CmdletBinding()] - param ( - [Parameter(Mandatory, ValueFromPipeline)] - [string] - $InputObject - ) - process { - # format of the 'choco list -r' output is: - # | (ie. adobereader|2015.6.7) - if (-not [string]::IsNullOrEmpty($InputObject)) { - $props = $_.split('|') - New-Object -TypeName psobject -Property @{ Name = $props[0]; Version = $props[1] } - } - } -} diff --git a/jenkins/scripts/Get-UpdatedPackage.ps1 b/jenkins/scripts/Get-UpdatedPackage.ps1 deleted file mode 100644 index 0e2aa01..0000000 --- a/jenkins/scripts/Get-UpdatedPackage.ps1 +++ /dev/null @@ -1,68 +0,0 @@ -[CmdletBinding()] -Param ( - [Parameter(Mandatory)] - [string] - $LocalRepo, - - [Parameter(Mandatory)] - [string] - $LocalRepoApiKey, - - [Parameter(Mandatory)] - [string] - $RemoteRepo -) - -. "$PSScriptRoot\ConvertTo-ChocoObject.ps1" - -if (([version] (choco --version).Split('-')[0]) -ge [version] '2.1.0') { - Write-Verbose "Clearing Chocolatey CLI cache to ensure latest package information is retrieved." - choco cache remove -} - -$LocalRepoSource = $(choco source --limit-output | ConvertFrom-Csv -Delimiter '|' -Header Name, Uri, Disabled).Where{ - $_.Uri -eq $LocalRepo -or - $_.Name -eq $LocalRepo -}[0] - -Write-Verbose "Getting list of local packages from '$LocalRepo'." -$localPkgs = choco search --source $LocalRepo -r | ConvertTo-ChocoObject -Write-Verbose "Retrieved list of $(($localPkgs).count) packages from '$Localrepo'." - -$localPkgs | ForEach-Object { - Write-Verbose "Getting remote package information for '$($_.name)'." - $remotePkg = choco search $_.name --source $RemoteRepo --exact -r | ConvertTo-ChocoObject - if ([version]($remotePkg.version) -gt ([version]$_.version)) { - Write-Verbose "Package '$($_.name)' has a remote version of '$($remotePkg.version)' which is later than the local version '$($_.version)'." - Write-Verbose "Internalizing package '$($_.name)' with version '$($remotePkg.version)'." - $tempPath = Join-Path -Path $env:TEMP -ChildPath ([GUID]::NewGuid()).GUID - choco download $_.name --no-progress --internalize --force --internalize-all-urls --append-use-original-location --output-directory=$tempPath --source=$RemoteRepo - - if ($LASTEXITCODE -eq 0) { - try { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source enable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} - - Write-Verbose "Pushing package '$($_.name)' to local repository '$LocalRepo'." - (Get-Item -Path (Join-Path -Path $tempPath -ChildPath "*.nupkg")).fullname | ForEach-Object { - choco push $_ --source $LocalRepo --api-key $LocalRepoApiKey --force - if ($LASTEXITCODE -eq 0) { - Write-Verbose "Package '$_' pushed to '$LocalRepo'." - } - else { - Write-Verbose "Package '$_' could not be pushed to '$LocalRepo'.`nThis could be because it already exists in the repository at a higher version and can be mostly ignored. Check error logs." - } - } - } finally { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source disable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} - } - } - else { - Write-Verbose "Failed to download package '$($_.name)'" - } - Remove-Item $tempPath -Recurse -Force - } - - else { - Write-Verbose "Package '$($_.name)' has a remote version of '$($remotePkg.version)' which is not later than the local version '$($_.version)'." - } -} diff --git a/jenkins/scripts/Invoke-ChocolateyInternalizer.ps1 b/jenkins/scripts/Invoke-ChocolateyInternalizer.ps1 deleted file mode 100644 index f2d5561..0000000 --- a/jenkins/scripts/Invoke-ChocolateyInternalizer.ps1 +++ /dev/null @@ -1,68 +0,0 @@ -<# - .SYNOPSIS - Internalizes packages from the community repository to an internal location. - - .DESCRIPTION - Internalizes packages from a specified repository (the Chocolatey Community - repository by default) into the target internal repository. All download - URLs and necessary resources are internalized to create a self-contained - package. - - .EXAMPLE - ./Internalizer.ps1 -Package googlechrome -RepositoryUrl https://chocoserver:8443/repository/ChocolateyInternal/index.json -LocalRepoApiKey 61332b06-d849-476c-b2ab-a290372c17d7 -#> -[CmdletBinding()] -param( - # The package(s) you want to internalize (comma separated). - [Parameter(Mandatory, Position = 0, ValueFromPipeline)] - [string[]] - $Package, - - # Your internal nuget repository url to push packages to. - [Parameter(Mandatory, Position = 1)] - [string] - $RepositoryUrl, - - # The API key of your internal repository server. - [Parameter(Mandatory, Position = 3)] - [string] - $NexusApiKey, - - # The remote repo to check against. Defaults to https://community.chocolatey.org/api/v2 - [Parameter(Position = 2)] - [string] - $RemoteRepo = 'https://community.chocolatey.org/api/v2/' -) -begin { - if (!(Test-Path "$env:ChocolateyInstall\license")) { - throw "Licensed edition required to use Package Internalizer" - } - - $Guid = [Guid]::NewGuid().Guid - $TempFolder = [IO.Path]::GetTempPath() | - Join-Path -ChildPath $Guid | - New-Item -ItemType Directory -Path { $_ } | - Select-Object -ExpandProperty FullName - - $LocalRepoSource = $(choco source --limit-output | ConvertFrom-Csv -Delimiter '|' -Header Name, Uri, Disabled).Where{ - $_.Uri -eq $RepositoryUrl - }[0] -} -process { - foreach ($item in $Package) { - choco download $item --internalize --output-directory="'$TempFolder'" --no-progress --internalize-all-urls --append-use-original-location --source="'$RemoteRepo'" - try { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source enable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} - - Get-ChildItem -Path $TempFolder -Filter *.nupkg -Recurse -File | ForEach-Object { - choco push $_.Fullname --source="'$RepositoryUrl'" --api-key="'$NexusApiKey'" --force - Remove-Item -Path $_.FullName -Force - } - } finally { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source disable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} - } - } -} -end { - Remove-Item -Path $TempFolder -Recurse -Force -} diff --git a/jenkins/scripts/Update-ProdRepoFromTest.ps1 b/jenkins/scripts/Update-ProdRepoFromTest.ps1 deleted file mode 100644 index 65a0e1f..0000000 --- a/jenkins/scripts/Update-ProdRepoFromTest.ps1 +++ /dev/null @@ -1,81 +0,0 @@ -[CmdletBinding()] -param ( - [Parameter(Mandatory)] - [string] - $ProdRepo, - - [Parameter(Mandatory)] - [string] - $ProdRepoApiKey, - - [Parameter(Mandatory)] - [string] - $TestRepo -) - -if (([version] (choco --version).Split('-')[0]) -ge [version] '2.1.0') { - Write-Verbose "Clearing Chocolatey CLI cache to ensure latest package information is retrieved." - choco cache remove -} - -$LocalRepoSource = $(choco source --limit-output | ConvertFrom-Csv -Delimiter '|' -Header Name, Uri, Disabled).Where{ - $_.Uri -eq $TestRepo -or - $_.Name -eq $TestRepo -}[0] - -Write-Verbose "Checking the list of packages available in the test and prod repositories" -try { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source enable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} - $testPkgs = choco search --source $TestRepo --all-versions --limit-output | ConvertFrom-Csv -Delimiter '|' -Header Name, Version -} finally { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source disable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} -} -$prodPkgs = choco search --source $ProdRepo --all-versions --limit-output | ConvertFrom-Csv -Delimiter '|' -Header Name, Version -$tempPath = Join-Path -Path $env:TEMP -ChildPath ([GUID]::NewGuid()).GUID - -$Packages = if ($null -eq $testPkgs) { - Write-Verbose "Test repository appears to be empty. Nothing to push to production." -} -elseif ($null -eq $prodPkgs) { - $testPkgs -} -else { - Compare-Object -ReferenceObject $testpkgs -DifferenceObject $prodpkgs -Property name, version | Where-Object SideIndicator -EQ '<=' -} - -$Packages | ForEach-Object { - Write-Verbose "Downloading package '$($_.Name)' v$($_.Version) to '$tempPath'." - try { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source enable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} - choco download $_.Name --version $_.Version --no-progress --output-directory=$tempPath --source=$TestRepo --ignore-dependencies - } finally { - if ([bool]::Parse($LocalRepoSource.Disabled)) {choco source disable --name="$($LocalRepoSource.Name)" -r | Write-Verbose} - } - - if ($LASTEXITCODE -eq 0) { - $pkgPath = (Get-Item -Path (Join-Path -Path $tempPath -ChildPath '*.nupkg')).FullName - # ####################### - # INSERT CODE HERE TO TEST YOUR PACKAGE - # ####################### - - # If package testing is successful ... - if ($LASTEXITCODE -eq 0) { - Write-Verbose "Pushing downloaded package '$(Split-Path -Path $pkgPath -Leaf)' to production repository '$ProdRepo'." - choco push $pkgPath --source=$ProdRepo --api-key=$ProdRepoApiKey --force - - if ($LASTEXITCODE -eq 0) { - Write-Verbose "Pushed package successfully." - } - else { - Write-Verbose "Could not push package." - } - } - else { - Write-Verbose "Package testing failed." - } - Remove-Item -Path $tempPath -Recurse -Force - } - else { - Write-Verbose "Could not download package." - } -} diff --git a/modules/C4B-Environment/C4B-Environment.psd1 b/modules/C4B-Environment/C4B-Environment.psd1 index 3c1a18b..d601de1 100644 --- a/modules/C4B-Environment/C4B-Environment.psd1 +++ b/modules/C4B-Environment/C4B-Environment.psd1 @@ -116,7 +116,9 @@ PrivateData = @{ # RequireLicenseAcceptance = $false # External dependent modules of this module - # ExternalModuleDependencies = @() + ExternalModuleDependencies = @( + "NexuShell" + ) } # End of PSData hashtable diff --git a/modules/C4B-Environment/C4B-Environment.psm1 b/modules/C4B-Environment/C4B-Environment.psm1 index dbbfee6..c8ce91f 100644 --- a/modules/C4B-Environment/C4B-Environment.psm1 +++ b/modules/C4B-Environment/C4B-Environment.psm1 @@ -39,6 +39,129 @@ function Invoke-Choco { } } +function Test-CertificateDomain { + param( + [Parameter(Mandatory)] + [string]$Thumbprint + ) + # Check the certificate exists + if (-not ($Certificate = Get-Item Cert:\LocalMachine\TrustedPeople\$Thumbprint)) { + throw "Certificate could not be found in Cert:\LocalMachine\TrustedPeople\. Please ensure it is is present, and try again." + } + + # Check that we have a domain for it + if (-not ($CertificateDnsName = Get-ChocoEnvironmentProperty CertSubject) -and ($Certificate.Subject -match '^CN=\*')) { + $matcher = 'CN\s?=\s?(?[^,\s]+)' + $null = $Certificate.Subject -match $matcher + $CertificateDnsName = if ($Matches.Subject.StartsWith('*')) { + # This is a wildcard cert, we need to prompt for the intended CertificateDnsName + while ($CertificateDnsName -notlike $Matches.Subject) { + $CertificateDnsName = Read-Host -Prompt "$(if ($CertificateDnsName) {"'$($CertificateDnsName)' is not a subdomain of '$($Matches.Subject)'. "})Please provide an FQDN to use with the certificate '$($Matches.Subject)'" + } + $CertificateDnsName + } else { + $Matches.Subject + } + Set-ChocoEnvironmentProperty CertSubject $CertificateDnsName + } + + $true +} + +function Wait-Site { + <# + .Synopsis + Waits for a given site to be available. A simple healthcheck. + #> + [Alias('Wait-Nexus','Wait-CCM','Wait-Jenkins')] + [CmdletBinding(DefaultParameterSetName="Name")] + param( + # The service name to check for a 200 response + [Parameter(ParameterSetName='Name', Position=0)] + [ValidateSet('Nexus','CCM','Jenkins')] + [string]$Name = $MyInvocation.InvocationName.Split('-')[-1], + + # The Url to check for a 200 response + [Parameter(ParameterSetName='Url', Mandatory, Position=0)] + [string]$Url = @{ + 'Nexus' = { + try { + Get-NexusLocalServiceUri + } catch { + Write-Verbose "Nexus may not be installed yet." + "http://localhost:8081" + } + } + 'CCM' = { + try { + $Binding = Get-WebBinding -Name ChocolateyCentralManagement + $Domain = if ( + $Binding.protocol -eq 'https' -and + ($Certificate = Get-ChildItem Cert:\LocalMachine\TrustedPeople | Where-Object Subject -notlike 'CN=`**').Count -eq 1 -and + $Certificate.Subject -match "^CN=(?.+)(?:,|$)" + ) { + $Matches.Domain + } elseif ($Binding.protocol -eq 'https' -and ($CertSubject = Get-ChocoEnvironmentProperty CertSubject)) { + $CertSubject + } else { + 'localhost' + } + "$($Binding.protocol)://$($Domain):$($Binding.bindingInformation.Trim('*').Trim(':'))/" + } catch { + Write-Verbose "CCM may not be installed yet." + "http://localhost" + } + } + 'Jenkins' = { + try { + if (Test-Path "C:\Program Files\Jenkins\jenkins.xml") { + [xml]$Xml = Get-Content "C:\Program Files\Jenkins\jenkins.xml" + if ($Xml.SelectSingleNode("/service/arguments").'#text' -match "--(?https?)Port=(?\d+)\b") { + $Port = $Matches.PortNumber + $Scheme = $Matches.Scheme + } + $Domain = if ($Scheme -eq 'https') { + Get-ChocoEnvironmentProperty CertSubject + } else { + 'localhost' + } + "$($Scheme)://$($Domain):$($Port)/login" # TODO: Get PATH + } elseif (Test-Path "C:\Program Files\Jenkins\jenkins.model.JenkinsLocationConfiguration.xml") { + [xml]$Location = (Get-Content "C:\Program Files\Jenkins\jenkins.model.JenkinsLocationConfiguration.xml" -ErrorAction Stop) -replace "^\<\?xml version=['""]1\.1['""]"," - [CmdletBinding(DefaultParameterSetName = "PathFullName")] - [OutputType([string])] - param( - # Path to the archive - [Parameter(Mandatory, ParameterSetName = "PathFullName")] - [Parameter(Mandatory, ParameterSetName = "PathName")] - [string]$Path, - - # Zip object for the archive - [Parameter(Mandatory, ParameterSetName = "ZipFullName", ValueFromPipelineByPropertyName)] - [Parameter(Mandatory, ParameterSetName = "ZipName", ValueFromPipelineByPropertyName)] - [Alias("Archive")] - [IO.Compression.ZipArchive]$Zip, - - # Name of the file(s) to remove from the archive - [Parameter(Mandatory, ParameterSetName = "PathFullName", ValueFromPipelineByPropertyName)] - [Parameter(Mandatory, ParameterSetName = "ZipFullName", ValueFromPipelineByPropertyName)] - [string]$FullName, - - # Name of the file(s) to remove from the archive - [Parameter(Mandatory, ParameterSetName = "PathName")] - [Parameter(Mandatory, ParameterSetName = "ZipName")] - [string]$Name - ) - begin { - if (-not $PSCmdlet.ParameterSetName.StartsWith("Zip")) { - $Stream = [IO.FileStream]::new($Path, [IO.FileMode]::Open) - $Zip = [IO.Compression.ZipArchive]::new($Stream, [IO.Compression.ZipArchiveMode]::Read) - } - } - process { - if (-not $FullName) { - $MatchingEntries = $Zip.Entries | Where-Object {$_.Name -eq $Name} - if ($MatchingEntries.Count -ne 1) { - Write-Error "File '$Name' not found in archive" -ErrorAction Stop - } - $FullName = $MatchingEntries[0].FullName - } - [System.IO.StreamReader]::new( - $Zip.GetEntry($FullName).Open() - ).ReadToEnd() - } - end { - if (-not $PSCmdlet.ParameterSetName.StartsWith("Zip")) { - $Zip.Dispose() - $Stream.Close() - $Stream.Dispose() - } - } -} - -function Get-ChocolateyPackageMetadata { - [CmdletBinding(DefaultParameterSetName='All')] - param( - # The folder or nupkg to check - [Parameter(Mandatory, Position=0, ValueFromPipelineByPropertyName)] - [string]$Path, - - # If provided, filters found packages by ID - [Parameter(Mandatory, Position=1, ParameterSetName='Id')] - [SupportsWildcards()] - [Alias('Name')] - [string]$Id = '*' - ) - process { - Get-ChildItem $Path -Filter $Id*.nupkg | ForEach-Object { - ([xml](Find-FileInArchive -Path $_.FullName -Like *.nuspec | Get-FileContentInArchive)).package.metadata | Where-Object Id -like $Id - } - } -} -#endregion - -#region Nexus functions (Start-C4BNexusSetup.ps1) -function Wait-Nexus { - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::tls12 - Do { - $response = try { - Invoke-WebRequest $("http://localhost:8081") -ErrorAction Stop - } - catch { - $null - } - - } until($response.StatusCode -eq '200') - Write-Host "Nexus is ready!" - -} - -function Invoke-NexusScript { - - [CmdletBinding()] - Param ( - [Parameter(Mandatory)] - [String] - $ServerUri, - - [Parameter(Mandatory)] - [Hashtable] - $ApiHeader, - - [Parameter(Mandatory)] - [String] - $Script - ) - - $scriptName = [GUID]::NewGuid().ToString() - $body = @{ - name = $scriptName - type = 'groovy' - content = $Script - } - - # Call the API - $baseUri = "$ServerUri/service/rest/v1/script" - - #Store the Script - $uri = $baseUri - Invoke-RestMethod -Uri $uri -ContentType 'application/json' -Body $($body | ConvertTo-Json) -Header $ApiHeader -Method Post - #Run the script - $uri = "{0}/{1}/run" -f $baseUri, $scriptName - $result = Invoke-RestMethod -Uri $uri -ContentType 'text/plain' -Header $ApiHeader -Method Post - #Delete the Script - $uri = "{0}/{1}" -f $baseUri, $scriptName - Invoke-RestMethod -Uri $uri -Header $ApiHeader -Method Delete -UseBasicParsing - - $result - -} - -function Connect-NexusServer { - <# - .SYNOPSIS - Creates the authentication header needed for REST calls to your Nexus server - - .DESCRIPTION - Creates the authentication header needed for REST calls to your Nexus server - - .PARAMETER Hostname - The hostname or ip address of your Nexus server - - .PARAMETER Credential - The credentials to authenticate to your Nexus server - - .PARAMETER UseSSL - Use https instead of http for REST calls. Defaults to 8443. - - .PARAMETER Sslport - If not the default 8443 provide the current SSL port your Nexus server uses - - .EXAMPLE - Connect-NexusServer -Hostname nexus.fabrikam.com -Credential (Get-Credential) - .EXAMPLE - Connect-NexusServer -Hostname nexus.fabrikam.com -Credential (Get-Credential) -UseSSL - .EXAMPLE - Connect-NexusServer -Hostname nexus.fabrikam.com -Credential $Cred -UseSSL -Sslport 443 - #> - [cmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/Connect-NexusServer/')] - param( - [Parameter(Mandatory, Position = 0)] - [Alias('Server')] - [String] - $Hostname, - - [Parameter(Mandatory, Position = 1)] - [System.Management.Automation.PSCredential] - $Credential, - - [Parameter()] - [Switch] - $UseSSL, - - [Parameter()] - [String] - $Sslport = '8443' - ) - - process { - - if ($UseSSL) { - $script:protocol = 'https' - $script:port = $Sslport - } - else { - $script:protocol = 'http' - $script:port = '8081' - } - - $script:HostName = $Hostname - - $credPair = "{0}:{1}" -f $Credential.UserName, $Credential.GetNetworkCredential().Password - - $encodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($credPair)) - - $script:header = @{ Authorization = "Basic $encodedCreds" } - - try { - $url = "$($protocol)://$($Hostname):$($port)/service/rest/v1/status" - - $params = @{ - Headers = $header - ContentType = 'application/json' - Method = 'GET' - Uri = $url - } - - $null = Invoke-RestMethod @params -ErrorAction Stop - Write-Host "Connected to $Hostname" -ForegroundColor Green - } - - catch { - $_.Exception.Message - } - } -} - -function Invoke-Nexus { - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [String] - $UriSlug, - - [Parameter()] - [Hashtable] - $Body, - - [Parameter()] - [Array] - $BodyAsArray, - - [Parameter()] - [String] - $BodyAsString, - - [Parameter()] - [String] - $File, - - [Parameter()] - [String] - $ContentType = 'application/json', - - [Parameter(Mandatory)] - [String] - $Method, - - [hashtable] - $AdditionalHeaders = @{} - ) - process { - $UriBase = "$($protocol)://$($Hostname):$($port)" - $Uri = $UriBase + $UriSlug - $Params = @{ - Headers = $header + $AdditionalHeaders - ContentType = $ContentType - Uri = $Uri - Method = $Method - } - - if ($Body) { - $Params.Add('Body', $($Body | ConvertTo-Json -Depth 3)) - } - - if ($BodyAsArray) { - $Params.Add('Body', $($BodyAsArray | ConvertTo-Json -Depth 3)) - } - - if ($BodyAsString) { - $Params.Add('Body', $BodyAsString) - } - - if ($File) { - $Params.Remove('ContentType') - $Params.Add('InFile', $File) - } - - Invoke-RestMethod @Params - - - } -} - -function Get-NexusUserToken { - <# - .SYNOPSIS - Fetches a User Token for the provided credential - - .DESCRIPTION - Fetches a User Token for the provided credential - - .PARAMETER Credential - The Nexus user for which to receive a token - - .NOTES - This is a private function not exposed to the end user. - #> - [CmdletBinding()] - Param( - [Parameter(Mandatory)] - [PSCredential] - $Credential - ) - - process { - $UriBase = "$($protocol)://$($Hostname):$($port)" - - $slug = '/service/extdirect' - - $uri = $UriBase + $slug - - $data = @{ - action = 'rapture_Security' - method = 'authenticationToken' - data = @("$([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($($Credential.Username))))", "$([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($($Credential.GetNetworkCredential().Password))))") - type = 'rpc' - tid = 16 - } - - Write-Verbose ($data | ConvertTo-Json) - $result = Invoke-RestMethod -Uri $uri -Method POST -Body ($data | ConvertTo-Json) -ContentType 'application/json' -Headers $header - $token = $result.result.data - $token - } - -} - -function Get-NexusRepository { - <# - .SYNOPSIS - Returns info about configured Nexus repository - - .DESCRIPTION - Returns details for currently configured repositories on your Nexus server - - .PARAMETER Format - Query for only a specific repository format. E.g. nuget, maven2, or docker - - .PARAMETER Name - Query for a specific repository by name - - .EXAMPLE - Get-NexusRepository - .EXAMPLE - Get-NexusRepository -Format nuget - .EXAMPLE - Get-NexusRepository -Name CompanyNugetPkgs - #> - [cmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/Get-NexusRepository/', DefaultParameterSetName = "default")] - param( - [Parameter(ParameterSetName = "Format", Mandatory)] - [String] - [ValidateSet('apt', 'bower', 'cocoapods', 'conan', 'conda', 'docker', 'gitlfs', 'go', 'helm', 'maven2', 'npm', 'nuget', 'p2', 'pypi', 'r', 'raw', 'rubygems', 'yum')] - $Format, - - [Parameter(ParameterSetName = "Type", Mandatory)] - [String] - [ValidateSet('hosted', 'group', 'proxy')] - $Type, - - [Parameter(ParameterSetName = "Name", Mandatory)] - [String] - $Name - ) - - - begin { - - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - $urislug = "/service/rest/v1/repositories" - } - process { - - switch ($PSCmdlet.ParameterSetName) { - { $Format } { - $filter = { $_.format -eq $Format } - - $result = Invoke-Nexus -UriSlug $urislug -Method Get - $result | Where-Object $filter - - } - - { $Name } { - $filter = { $_.name -eq $Name } - - $result = Invoke-Nexus -UriSlug $urislug -Method Get - $result | Where-Object $filter - - } - - { $Type } { - $filter = { $_.type -eq $Type } - $result = Invoke-Nexus -UriSlug $urislug -Method Get - $result | Where-Object $filter - } - - default { - Invoke-Nexus -UriSlug $urislug -Method Get | ForEach-Object { - [pscustomobject]@{ - Name = $_.SyncRoot.name - Format = $_.SyncRoot.format - Type = $_.SyncRoot.type - Url = $_.SyncRoot.url - Attributes = $_.SyncRoot.attributes - } - } - } - } - } -} - -function Remove-NexusRepository { - <# - .SYNOPSIS - Removes a given repository from the Nexus instance - - .DESCRIPTION - Removes a given repository from the Nexus instance - - .PARAMETER Repository - The repository to remove - - .PARAMETER Force - Disable prompt for confirmation before removal - - .EXAMPLE - Remove-NexusRepository -Repository ProdNuGet - .EXAMPLE - Remove-NexusRepository -Repository MavenReleases -Force() - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/Remove-NexusRepository/', SupportsShouldProcess, ConfirmImpact = 'High')] - Param( - [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] - [Alias('Name')] - [ArgumentCompleter( { - param($command, $WordToComplete, $CommandAst, $FakeBoundParams) - $repositories = (Get-NexusRepository).Name - - if ($WordToComplete) { - $repositories.Where{ $_ -match "^$WordToComplete" } - } - else { - $repositories - } - })] - [String[]] - $Repository, - - [Parameter()] - [Switch] - $Force - ) - begin { - - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - $urislug = "/service/rest/v1/repositories" - } - process { - - $Repository | Foreach-Object { - $Uri = $urislug + "/$_" - - try { - - if ($Force -and -not $Confirm) { - $ConfirmPreference = 'None' - if ($PSCmdlet.ShouldProcess("$_", "Remove Repository")) { - $result = Invoke-Nexus -UriSlug $Uri -Method 'DELETE' -ErrorAction Stop - [pscustomobject]@{ - Status = 'Success' - Repository = $_ - } - } - } - else { - if ($PSCmdlet.ShouldProcess("$_", "Remove Repository")) { - $result = Invoke-Nexus -UriSlug $Uri -Method 'DELETE' -ErrorAction Stop - [pscustomobject]@{ - Status = 'Success' - Repository = $_ - Timestamp = $result.date - } - } - } - } - - catch { - $_.exception.message - } - } - } -} - -function Remove-NexusRepositoryFolder { - <# - .SYNOPSIS - Removes a given folder from a repository from the Nexus instance - - .PARAMETER RepositoryName - The repository to remove from - - .PARAMETER Name - The name of the folder to remove - - .EXAMPLE - Remove-NexusRepositoryFolder -RepositoryName MyNuGetRepo -Name 'v3' - # Removes the v3 folder in the MyNuGetRepo repository - #> - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [string]$RepositoryName, - - [Parameter(Mandatory)] - [string]$Name - ) - end { - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - $ApiParameters = @{ - UriSlug = "/service/extdirect" - Method = "POST" - Body = @{ - action = "coreui_Component" - method = "deleteFolder" - data = @( - $Name, - $RepositoryName - ) - type = "rpc" - tid = Get-Random -Minimum 1 -Maximum 100 - } - AdditionalHeaders = @{ - "X-Nexus-UI" = "true" - } - } - - $Result = Invoke-Nexus @ApiParameters - - if (-not $Result.result.success) { - throw "Failed to delete folder: $($Result.result.message)" - } - } -} - -function New-NexusNugetHostedRepository { - <# - .SYNOPSIS - Creates a new NuGet Hosted repository - - .DESCRIPTION - Creates a new NuGet Hosted repository - - .PARAMETER Name - The name of the repository - - .PARAMETER CleanupPolicy - The Cleanup Policies to apply to the repository - - - .PARAMETER Online - Marks the repository to accept incoming requests - - .PARAMETER BlobStoreName - Blob store to use to store NuGet packages - - .PARAMETER StrictContentValidation - Validate that all content uploaded to this repository is of a MIME type appropriate for the repository format - - .PARAMETER DeploymentPolicy - Controls if deployments of and updates to artifacts are allowed - - .PARAMETER HasProprietaryComponents - Components in this repository count as proprietary for namespace conflict attacks (requires Sonatype Nexus Firewall) - - .EXAMPLE - New-NexusNugetHostedRepository -Name NugetHostedTest -DeploymentPolicy Allow - .EXAMPLE - $RepoParams = @{ - Name = MyNuGetRepo - CleanupPolicy = '90 Days' - DeploymentPolicy = 'Allow' - UseStrictContentValidation = $true - } - - New-NexusNugetHostedRepository @RepoParams - .NOTES - General notes - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/New-NexusNugetHostedRepository/')] - Param( - [Parameter(Mandatory)] - [String] - $Name, - - [Parameter()] - [String] - $CleanupPolicy, - - [Parameter()] - [Switch] - $Online = $true, - - [Parameter()] - [String] - $BlobStoreName = 'default', - - [Parameter()] - [ValidateSet('True', 'False')] - [String] - $UseStrictContentValidation = 'True', - - [Parameter()] - [ValidateSet('Allow', 'Deny', 'Allow_Once')] - [String] - $DeploymentPolicy, - - [Parameter()] - [Switch] - $HasProprietaryComponents - ) - - begin { - - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - $urislug = "/service/rest/v1/repositories" - - } - - process { - $formatUrl = $urislug + '/nuget' - - $FullUrlSlug = $formatUrl + '/hosted' - - - $body = @{ - name = $Name - online = [bool]$Online - storage = @{ - blobStoreName = $BlobStoreName - strictContentTypeValidation = $UseStrictContentValidation - writePolicy = $DeploymentPolicy - } - cleanup = @{ - policyNames = @($CleanupPolicy) - } - } - - if ($HasProprietaryComponents) { - $Prop = @{ - proprietaryComponents = 'True' - } - - $Body.Add('component', $Prop) - } - - Write-Verbose $($Body | ConvertTo-Json) - $null = Invoke-Nexus -UriSlug $FullUrlSlug -Body $Body -Method POST - - } -} - -function New-NexusRawHostedRepository { - <# - .SYNOPSIS - Creates a new Raw Hosted repository - - .DESCRIPTION - Creates a new Raw Hosted repository - - .PARAMETER Name - The Name of the repository to create - - .PARAMETER Online - Mark the repository as Online. Defaults to True - - .PARAMETER BlobStore - The blob store to attach the repository too. Defaults to 'default' - - .PARAMETER UseStrictContentTypeValidation - Validate that all content uploaded to this repository is of a MIME type appropriate for the repository format - - .PARAMETER DeploymentPolicy - Controls if deployments of and updates to artifacts are allowed - - .PARAMETER CleanupPolicy - Components that match any of the Applied policies will be deleted - - .PARAMETER HasProprietaryComponents - Components in this repository count as proprietary for namespace conflict attacks (requires Sonatype Nexus Firewall) - - .PARAMETER ContentDisposition - Add Content-Disposition header as 'Attachment' to disable some content from being inline in a browser. - - .EXAMPLE - New-NexusRawHostedRepository -Name BinaryArtifacts -ContentDisposition Attachment - .EXAMPLE - $RepoParams = @{ - Name = 'BinaryArtifacts' - Online = $true - UseStrictContentTypeValidation = $true - DeploymentPolicy = 'Allow' - CleanupPolicy = '90Days', - BlobStore = 'AmazonS3Bucket' - } - New-NexusRawHostedRepository @RepoParams - - .NOTES - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/New-NexusRawHostedRepository/', DefaultParameterSetname = "Default")] - Param( - [Parameter(Mandatory)] - [String] - $Name, - - [Parameter()] - [Switch] - $Online = $true, - - [Parameter()] - [String] - $BlobStore = 'default', - - [Parameter()] - [Switch] - $UseStrictContentTypeValidation, - - [Parameter()] - [ValidateSet('Allow', 'Deny', 'Allow_Once')] - [String] - $DeploymentPolicy = 'Allow_Once', - - [Parameter()] - [String] - $CleanupPolicy, - - [Parameter()] - [Switch] - $HasProprietaryComponents, - - [Parameter(Mandatory)] - [ValidateSet('Inline', 'Attachment')] - [String] - $ContentDisposition - ) - - begin { - - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - $urislug = "/service/rest/v1/repositories/raw/hosted" - - } - - process { - - $Body = @{ - name = $Name - online = [bool]$Online - storage = @{ - blobStoreName = $BlobStore - strictContentTypeValidation = [bool]$UseStrictContentTypeValidation - writePolicy = $DeploymentPolicy.ToLower() - } - cleanup = @{ - policyNames = @($CleanupPolicy) - } - component = @{ - proprietaryComponents = [bool]$HasProprietaryComponents - } - raw = @{ - contentDisposition = $ContentDisposition.ToUpper() - } - } - - Write-Verbose $($Body | ConvertTo-Json) - $null = Invoke-Nexus -UriSlug $urislug -Body $Body -Method POST - - - } -} - -function Get-NexusRealm { - <# - .SYNOPSIS - Gets Nexus Realm information - - .DESCRIPTION - Gets Nexus Realm information - - .PARAMETER Active - Returns only active realms - - .EXAMPLE - Get-NexusRealm - .EXAMPLE - Get-NexusRealm -Active - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/Get-NexusRealm/')] - Param( - [Parameter()] - [Switch] - $Active - ) - - begin { - - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - - $urislug = "/service/rest/v1/security/realms/available" - - - } - - process { - - if ($Active) { - $current = Invoke-Nexus -UriSlug $urislug -Method 'GET' - $urislug = '/service/rest/v1/security/realms/active' - $Activated = Invoke-Nexus -UriSlug $urislug -Method 'GET' - $current | Where-Object { $_.Id -in $Activated } - } - else { - $result = Invoke-Nexus -UriSlug $urislug -Method 'GET' - - $result | Foreach-Object { - [pscustomobject]@{ - Id = $_.id - Name = $_.name - } - } - } - } -} - -function Enable-NexusRealm { - <# - .SYNOPSIS - Enable realms in Nexus - - .DESCRIPTION - Enable realms in Nexus - - .PARAMETER Realm - The realms you wish to activate - - .EXAMPLE - Enable-NexusRealm -Realm 'NuGet Api-Key Realm', 'Rut Auth Realm' - .EXAMPLE - Enable-NexusRealm -Realm 'LDAP Realm' - - .NOTES - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/Enable-NexusRealm/')] - Param( - [Parameter(Mandatory)] - [ArgumentCompleter( { - param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) - - $r = (Get-NexusRealm).name - - if ($WordToComplete) { - $r.Where($_ -match "^$WordToComplete") - } - else { - $r - } - } - )] - [String[]] - $Realm - ) - - begin { - - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - $urislug = "/service/rest/v1/security/realms/active" - - } - - process { - - $collection = @() - - Get-NexusRealm -Active | ForEach-Object { $collection += $_.id } - - $Realm | Foreach-Object { - - switch ($_) { - 'Conan Bearer Token Realm' { $id = 'org.sonatype.repository.conan.internal.security.token.ConanTokenRealm' } - 'Default Role Realm' { $id = 'DefaultRole' } - 'Docker Bearer Token Realm' { $id = 'DockerToken' } - 'LDAP Realm' { $id = 'LdapRealm' } - 'Local Authentication Realm' { $id = 'NexusAuthenticatingRealm' } - 'Local Authorizing Realm' { $id = 'NexusAuthorizingRealm' } - 'npm Bearer Token Realm' { $id = 'NpmToken' } - 'NuGet API-Key Realm' { $id = 'NuGetApiKey' } - 'Rut Auth Realm' { $id = 'rutauth-realm' } - } - - $collection += $id - - } - - $body = $collection - - Write-Verbose $($Body | ConvertTo-Json) - $null = Invoke-Nexus -UriSlug $urislug -BodyAsArray $Body -Method PUT - - } -} - -function Get-NexusNuGetApiKey { - <# - .SYNOPSIS - Retrieves the NuGet API key of the given user credential - - .DESCRIPTION - Retrieves the NuGet API key of the given user credential - - .PARAMETER Credential - The Nexus User whose API key you wish to retrieve - - .EXAMPLE - Get-NexusNugetApiKey -Credential (Get-Credential) - - .NOTES - - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/TreasureChest/Security/API%20Key/Get-NexusNuGetApiKey/')] - Param( - [Parameter(Mandatory)] - [PSCredential] - $Credential - ) - - process { - $token = Get-NexusUserToken -Credential $Credential - $base64Token = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($token)) - $UriBase = "$($protocol)://$($Hostname):$($port)" - - $slug = "/service/rest/internal/nuget-api-key?authToken=$base64Token&_dc=$(([DateTime]::ParseExact("01/02/0001 21:08:29", "MM/dd/yyyy HH:mm:ss",$null)).Ticks)" - - $uri = $UriBase + $slug - - Invoke-RestMethod -Uri $uri -Method GET -ContentType 'application/json' -Headers $header - - } -} - -function New-NexusRawComponent { - <# - .SYNOPSIS - Uploads a file to a Raw repository - - .DESCRIPTION - Uploads a file to a Raw repository - - .PARAMETER RepositoryName - The Raw repository to upload too - - .PARAMETER File - The file to upload - - .PARAMETER Directory - The directory to store the file on the repo - - .PARAMETER Name - The name of the file stored into the repo. Can be different than the file name being uploaded. - - .EXAMPLE - New-NexusRawComponent -RepositoryName GeneralFiles -File C:\temp\service.1234.log - .EXAMPLE - New-NexusRawComponent -RepositoryName GeneralFiles -File C:\temp\service.log -Directory logs - .EXAMPLE - New-NexusRawComponent -RepositoryName GeneralFile -File C:\temp\service.log -Directory logs -Name service.99999.log - - .NOTES - #> - [CmdletBinding()] - Param( - [Parameter(Mandatory)] - [String] - $RepositoryName, - - [Parameter(Mandatory)] - [String] - $File, - - [Parameter()] - [String] - $Directory, - - [Parameter()] - [String] - $Name = (Split-Path -Leaf $File) - ) - - process { - - if (-not $Directory) { - $urislug = "/repository/$($RepositoryName)/$($Name)" - } - else { - $urislug = "/repository/$($RepositoryName)/$($Directory)/$($Name)" - - } - $UriBase = "$($protocol)://$($Hostname):$($port)" - $Uri = $UriBase + $UriSlug - - - $params = @{ - Uri = $Uri - Method = 'PUT' - ContentType = 'text/plain' - InFile = $File - Headers = $header - UseBasicParsing = $true - } - - $null = Invoke-WebRequest @params - } -} - -function Get-NexusUser { - <# - .SYNOPSIS - Retrieve a list of users. Note if the source is not 'default' the response is limited to 100 users. - - .DESCRIPTION - Retrieve a list of users. Note if the source is not 'default' the response is limited to 100 users. - - .PARAMETER User - The username to fetch - - .PARAMETER Source - The source to fetch from - - .EXAMPLE - Get-NexusUser - - .EXAMPLE - Get-NexusUser -User bob - - .EXAMPLE - Get-NexusUser -Source default - - .NOTES - - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/NexuShell/Security/User/Get-NexusUser/')] - Param( - [Parameter()] - [String] - $User, - - [Parameter()] - [String] - $Source - ) - - begin { - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - } - - process { - $urislug = '/service/rest/v1/security/users' - - if ($User) { - $urislug = "/service/rest/v1/security/users?userId=$User" - } - - if ($Source) { - $urislug = "/service/rest/v1/security/users?source=$Source" - } - - if ($User -and $Source) { - $urislug = "/service/rest/v1/security/users?userId=$User&source=$Source" - } - - $result = Invoke-Nexus -Urislug $urislug -Method GET - - $result | Foreach-Object { - [pscustomobject]@{ - Username = $_.userId - FirstName = $_.firstName - LastName = $_.lastName - EmailAddress = $_.emailAddress - Source = $_.source - Status = $_.status - ReadOnly = $_.readOnly - Roles = $_.roles - ExternalRoles = $_.externalRoles - } - } - } -} - -function Get-NexusRole { - <# - .SYNOPSIS - Retrieve Nexus Role information - - .DESCRIPTION - Retrieve Nexus Role information - - .PARAMETER Role - The role to retrieve - - .PARAMETER Source - The source to retrieve from - - .EXAMPLE - Get-NexusRole - - .EXAMPLE - Get-NexusRole -Role ExampleRole - - .NOTES - - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/NexuShell/Security/Roles/Get-NexusRole/')] - Param( - [Parameter()] - [Alias('id')] - [String] - $Role, - - [Parameter()] - [String] - $Source - ) - begin { if (-not $header) { throw 'Not connected to Nexus server! Run Connect-NexusServer first.' } } - process { - - $urislug = '/service/rest/v1/security/roles' - - if ($Role) { - $urislug = "/service/rest/v1/security/roles/$Role" - } - - if ($Source) { - $urislug = "/service/rest/v1/security/roles?source=$Source" - } - - if ($Role -and $Source) { - $urislug = "/service/rest/v1/security/roles/$($Role)?source=$Source" - } - - Write-verbose $urislug - $result = Invoke-Nexus -Urislug $urislug -Method GET - - $result | ForEach-Object { - [PSCustomObject]@{ - Id = $_.id - Source = $_.source - Name = $_.name - Description = $_.description - Privileges = $_.privileges - Roles = $_.roles - } - } - } -} - -function New-NexusUser { - <# - .SYNOPSIS - Create a new user in the default source. - - .DESCRIPTION - Create a new user in the default source. - - .PARAMETER Username - The userid which is required for login. This value cannot be changed. - - .PARAMETER Password - The password for the new user. - - .PARAMETER FirstName - The first name of the user. - - .PARAMETER LastName - The last name of the user. - - .PARAMETER EmailAddress - The email address associated with the user. - - .PARAMETER Status - The user's status, e.g. active or disabled. - - .PARAMETER Roles - The roles which the user has been assigned within Nexus. - - .EXAMPLE - $params = @{ - Username = 'jimmy' - Password = ("sausage" | ConvertTo-SecureString -AsPlainText -Force) - FirstName = 'Jimmy' - LastName = 'Dean' - EmailAddress = 'sausageking@jimmydean.com' - Status = Active - Roles = 'nx-admin' - } - - New-NexusUser @params - - .NOTES - - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/NexuShell/Security/User/New-NexusUser/')] - Param( - [Parameter(Mandatory)] - [String] - $Username, - - [Parameter(Mandatory)] - [SecureString] - $Password, - - [Parameter(Mandatory)] - [String] - $FirstName, - - [Parameter(Mandatory)] - [String] - $LastName, - - [Parameter(Mandatory)] - [String] - $EmailAddress, - - [Parameter(Mandatory)] - [ValidateSet('Active', 'Locked', 'Disabled', 'ChangePassword')] - [String] - $Status, - - [Parameter(Mandatory)] - [ArgumentCompleter({ - param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) - (Get-NexusRole).Id.Where{ $_ -like "*$WordToComplete*" } - })] - [String[]] - $Roles - ) - - process { - $urislug = '/service/rest/v1/security/users' - - $Body = @{ - userId = $Username - firstName = $FirstName - lastName = $LastName - emailAddress = $EmailAddress - password = [System.Net.NetworkCredential]::new($Username, $Password).Password - status = $Status - roles = $Roles - } - - Write-Verbose ($Body | ConvertTo-Json) - $result = Invoke-Nexus -Urislug $urislug -Body $Body -Method POST - - [pscustomObject]@{ - Username = $result.userId - FirstName = $result.firstName - LastName = $result.lastName - EmailAddress = $result.emailAddress - Source = $result.source - Status = $result.status - Roles = $result.roles - ExternalRoles = $result.externalRoles + if ($Stream) { + $Stream.Close() + $Stream.Dispose() } } } -function New-NexusRole { +function Get-FileContentInArchive { <# - .SYNOPSIS - Creates a new Nexus Role - - .DESCRIPTION - Creates a new Nexus Role - - .PARAMETER Id - The ID of the role - - .PARAMETER Name - The friendly name of the role - - .PARAMETER Description - A description of the role - - .PARAMETER Privileges - Included privileges for the role - - .PARAMETER Roles - Included nested roles - - .EXAMPLE - New-NexusRole -Id SamepleRole - - .EXAMPLE - New-NexusRole -Id SampleRole -Description "A sample role" -Privileges nx-all - - .NOTES - + .Synopsis + Returns the content of a file from within an archive + .Example + Get-FileContentInArchive -Path $ZipPath -Name "chocolateyInstall.ps1" + .Example + Get-FileContentInArchive -Zip $Zip -FullName "tools\chocolateyInstall.ps1" + .Example + Find-FileInArchive -Path $ZipPath -Like *.nuspec | Get-FileContentInArchive #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/NexuShell/Security/Roles/New-NexusRole/')] - Param( - [Parameter(Mandatory)] - [String] - $Id, - - [Parameter(Mandatory)] - [String] - $Name, + [CmdletBinding(DefaultParameterSetName = "PathFullName")] + [OutputType([string])] + param( + # Path to the archive + [Parameter(Mandatory, ParameterSetName = "PathFullName")] + [Parameter(Mandatory, ParameterSetName = "PathName")] + [string]$Path, - [Parameter()] - [String] - $Description, + # Zip object for the archive + [Parameter(Mandatory, ParameterSetName = "ZipFullName", ValueFromPipelineByPropertyName)] + [Parameter(Mandatory, ParameterSetName = "ZipName", ValueFromPipelineByPropertyName)] + [Alias("Archive")] + [IO.Compression.ZipArchive]$Zip, - [Parameter(Mandatory)] - [String[]] - $Privileges, + # Name of the file(s) to remove from the archive + [Parameter(Mandatory, ParameterSetName = "PathFullName", ValueFromPipelineByPropertyName)] + [Parameter(Mandatory, ParameterSetName = "ZipFullName", ValueFromPipelineByPropertyName)] + [string]$FullName, - [Parameter()] - [String[]] - $Roles + # Name of the file(s) to remove from the archive + [Parameter(Mandatory, ParameterSetName = "PathName")] + [Parameter(Mandatory, ParameterSetName = "ZipName")] + [string]$Name ) - begin { - if (-not $header) { - throw 'Not connected to Nexus server! Run Connect-NexusServer first.' - } + if (-not $PSCmdlet.ParameterSetName.StartsWith("Zip")) { + $Stream = [IO.FileStream]::new($Path, [IO.FileMode]::Open) + $Zip = [IO.Compression.ZipArchive]::new($Stream, [IO.Compression.ZipArchiveMode]::Read) + } } - process { - - $urislug = '/service/rest/v1/security/roles' - $Body = @{ - - id = $Id - name = $Name - description = $Description - privileges = @($Privileges) - roles = $Roles - - } - - Invoke-Nexus -Urislug $urislug -Body $Body -Method POST | Foreach-Object { - [PSCustomobject]@{ - Id = $_.id - Name = $_.name - Description = $_.description - Privileges = $_.privileges - Roles = $_.roles + if (-not $FullName) { + $MatchingEntries = $Zip.Entries | Where-Object {$_.Name -eq $Name} + if ($MatchingEntries.Count -ne 1) { + Write-Error "File '$Name' not found in archive" -ErrorAction Stop } + $FullName = $MatchingEntries[0].FullName + } + [System.IO.StreamReader]::new( + $Zip.GetEntry($FullName).Open() + ).ReadToEnd() + } + end { + if (-not $PSCmdlet.ParameterSetName.StartsWith("Zip")) { + $Zip.Dispose() + $Stream.Close() + $Stream.Dispose() } - } } -function Set-NexusAnonymousAuth { - <# - .SYNOPSIS - Turns Anonymous Authentication on or off in Nexus - - .DESCRIPTION - Turns Anonymous Authentication on or off in Nexus - - .PARAMETER Enabled - Turns on Anonymous Auth - - .PARAMETER Disabled - Turns off Anonymous Auth - - .EXAMPLE - Set-NexusAnonymousAuth -Enabled - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/NexuShell/Set-NexusAnonymousAuth/')] - Param( - [Parameter()] - [Switch] - $Enabled, +function Get-ChocolateyPackageMetadata { + [CmdletBinding(DefaultParameterSetName='All')] + param( + # The folder or nupkg to check + [Parameter(Mandatory, Position=0, ValueFromPipelineByPropertyName)] + [string]$Path, - [Parameter()] - [Switch] - $Disabled + # If provided, filters found packages by ID + [Parameter(Mandatory, Position=1, ParameterSetName='Id')] + [SupportsWildcards()] + [Alias('Name')] + [string]$Id = '*' ) - - begin { - - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." + process { + Get-ChildItem $Path -Filter $Id*.nupkg | ForEach-Object { + ([xml](Find-FileInArchive -Path $_.FullName -Like *.nuspec | Get-FileContentInArchive)).package.metadata | Where-Object Id -like $Id } - - $urislug = "/service/rest/v1/security/anonymous" } +} +#endregion - process { - - Switch ($true) { - - $Enabled { - $Body = @{ - enabled = $true - userId = 'anonymous' - realmName = 'NexusAuthorizingRealm' - } - - Invoke-Nexus -UriSlug $urislug -Body $Body -Method 'PUT' - } - - $Disabled { - $Body = @{ - enabled = $false - userId = 'anonymous' - realmName = 'NexusAuthorizingRealm' - } - - Invoke-Nexus -UriSlug $urislug -Body $Body -Method 'PUT' +#region Nexus functions (Start-C4BNexusSetup.ps1) +function Invoke-NexusScript { + [CmdletBinding()] + Param ( + [Parameter(Mandatory)] + [String] + $ServerUri, - } - } + [Parameter(Mandatory)] + [Hashtable] + $ApiHeader, + + [Parameter(Mandatory)] + [String] + $Script + ) + try { + $scriptName = [GUID]::NewGuid().ToString() + New-NexusScript -Name $scriptName -Content $Script -Type "groovy" + Start-NexusScript -Name $scriptName + } finally { + Remove-NexusScript -Name $scriptName } } @@ -1767,6 +540,9 @@ function Add-DatabaseUserAndRoles { USE [master] IF EXISTS(SELECT * FROM msdb.sys.syslogins WHERE UPPER([name]) = UPPER('$Username')) BEGIN +DECLARE @kill varchar(8000) = ''; +SELECT @kill = @kill + 'kill ' + CONVERT(varchar(5), session_id) + ';' FROM sys.dm_exec_sessions WHERE UPPER([login_name]) = UPPER('$Username') +EXEC(@kill); DROP LOGIN [$Username] END @@ -1800,21 +576,6 @@ ALTER ROLE [$DatabaseRole] ADD MEMBER [$Username] $Connection.Close() } -function New-CcmSalt { - [CmdletBinding()] - param( - [Parameter()] - [int] - $MinLength = 32, - [Parameter()] - [int] - $SpecialCharCount = 12 - ) - process { - [System.Web.Security.Membership]::GeneratePassword($MinLength, $SpecialCharCount) - } -} - function Stop-CCMService { #Stop Central Management components Stop-Service chocolatey-central-management @@ -1827,7 +588,7 @@ function Remove-CcmBinding { process { Write-Verbose "Removing existing bindings" - netsh http delete sslcert ipport=0.0.0.0:443 + netsh http delete sslcert ipport=0.0.0.0:443 | Write-Verbose } } @@ -1839,7 +600,7 @@ function New-CcmBinding { Write-Verbose "Adding new binding https://${SubjectWithoutCn} to Chocolatey Central Management" $guid = [Guid]::NewGuid().ToString("B") - netsh http add sslcert ipport=0.0.0.0:443 certhash=$Thumbprint certstorename=TrustedPeople appid="$guid" + netsh http add sslcert ipport=0.0.0.0:443 certhash=$Thumbprint certstorename=TrustedPeople appid="$guid" | Write-Verbose Get-WebBinding -Name ChocolateyCentralManagement | Remove-WebBinding New-WebBinding -Name ChocolateyCentralManagement -Protocol https -Port 443 -SslFlags 0 -IpAddress '*' } @@ -1877,6 +638,190 @@ function Set-CcmCertificate { } } +function Get-CcmAuthenticatedSession { + [CmdletBinding()] + [OutputType([Microsoft.PowerShell.Commands.WebRequestSession])] + param( + # The CCM server to operate against + [string]$CcmEndpoint = "http://localhost", + + # The current credential for the account to change + [System.Net.NetworkCredential]$Credential = @{ + userName = "ccmadmin" + password = "123qwe" + } + ) + end { + # Wait-CCM -Url $CcmEndpoint + + Write-Verbose "Authenticating to CCM Web at '$($CcmEndpoint)'" + $methodParams = @{ + Uri = "$CcmEndpoint/Account/Login" + Body = @{ + usernameOrEmailAddress = $Credential.Username + password = $Credential.Password + } + ContentType = 'application/x-www-form-urlencoded' + Method = "POST" + SessionVariable = "Session" + } + try { + $null = Invoke-WebRequest @methodParams -UseBasicParsing -ErrorAction Stop + } catch { + Write-Error "Failed to authenticate with '$($CcmEndpoint)': $($_)" + } + + $Session + } +} + +function Set-CcmAccountPassword { + <# + .Synopsis + Sets the password for a current CCM user + + .Notes + Relies on the account not being set to reset-password-on-next-login, and not locked out. + #> + [CmdletBinding()] + param( + # The CCM server to operate against + [string]$CcmEndpoint = "http://localhost", + + # The current credential for the account to change + [System.Net.NetworkCredential]$Credential = @{ + userName = "ccmadmin" + password = "123qwe" + }, + + # A Valid ConnectionString for the CCM Database + [string]$ConnectionString, + + # The new password to set + [Parameter(Mandatory)] + [SecureString]$NewPassword + ) + $NewCredential = [System.Net.NetworkCredential]::new($Credential.UserName, $NewPassword) + + if ($ConnectionString) { + try { + $Connection = [System.Data.SQLClient.SqlConnection]::new($ConnectionString) + $Connection.Open() + $Query = [System.Data.SQLClient.SqlCommand]::new( + "UPDATE [dbo].[AbpUsers] SET ShouldChangePasswordOnNextLogin = 0, IsLockoutEnabled = 0 WHERE Name = @UserName and TenantId = '1'", + $Connection + ) + $null = $Query.Parameters.Add( + [System.Data.SqlClient.SqlParameter]::new('UserName', $Credential.UserName) + ) + $QueryResult = $Query.BeginExecuteReader() + while (-not $QueryResult.isCompleted) { + Write-Verbose "Waiting for SQL Query to return" + Start-Sleep -Milliseconds 100 + } + if ($QueryResult.isCompleted -and -not $QueryResult.IsFaulted) { + Write-Verbose "Unset ShouldChangePasswordOnNextLogin for '$($Credential.Username)'" + } + } finally { + $Query.Dispose() + $Connection.Close() + $Connection.Dispose() + } + } + + $Session = Get-CcmAuthenticatedSession -CcmEndpoint $CcmEndpoint -Credential $Credential + + Write-Verbose "Changing password for account '$($Credential.UserName)'" + $resetParams = @{ + Uri = "$CcmEndpoint/api/services/app/Profile/ChangePassword" + Body = @{ + CurrentPassword = $Credential.Password + NewPassword = $NewCredential.Password + NewPasswordRepeat = $NewCredential.Password + } | ConvertTo-Json + ContentType = 'application/json' + Method = "POST" + WebSession = $Session + } + $Result = Invoke-RestMethod @resetParams -UseBasicParsing + + if ($Result.Success -eq 'true') { + Write-Verbose "Password for account '$($Credential.UserName)' was changed successfully." + } +} + +function Update-CcmSettings { + [CmdletBinding()] + param( + # The CCM server to operate against + [string]$CcmEndpoint = "http://localhost", + + # The current credential for the admin account + [System.Net.NetworkCredential]$Credential = @{ + userName = "ccmadmin" + password = "123qwe" + }, + + # A hashtable of settings to update. Only works two levels deep. + [hashtable]$Settings + ) + end { + $Session = Get-CcmAuthenticatedSession -CcmEndpoint $CcmEndpoint -Credential $Credential + + # Get Current Settings + $ServerSettings = (Invoke-RestMethod -Uri $CcmEndpoint/api/services/app/TenantSettings/GetAllSettings -WebSession $Session).result + + # Overwrite Settings via Hashtable + foreach ($Heading in $Settings.Keys) { + foreach ($Setting in $Settings[$Heading].Keys) { + $ServerSettings.$Heading.$Setting = $Settings.$Heading.$Setting + } + } + + # PUT new Settings to CCM + $SettingChange = @{ + Uri = "$CcmEndpoint/api/services/app/TenantSettings/UpdateAllSettings" + Method = "PUT" + ContentType = 'application/json; charset=utf-8' + Body = $ServerSettings | ConvertTo-Json + WebSession = $Session + } + $Result = Invoke-RestMethod @SettingChange -ErrorAction Stop + + if ($Result.success) { + Write-Verbose "Updated Settings successfully." + } + } +} + +function Set-CcmEncryptionPassword { + [CmdletBinding()] + param( + # The CCM server to operate against + [string]$CcmEndpoint = "http://localhost", + + # The current credential for the account to change + [System.Net.NetworkCredential]$Credential = @{ + userName = "ccmadmin" + password = "123qwe" + }, + + # New encryption password to set + [SecureString]$NewPassword, + + # Previous encryption password (unset on fresh install) + [SecureString]$OldPassword = [SecureString]::new() + ) + end { + Update-CcmSettings -CcmEndpoint $CcmEndpoint -Credential $Credential -Settings @{ + encryption = @{ + oldPassphrase = $OldPassword.ToPlainText() + passphrase = $NewPassword.ToPlainText() + confirmPassphrase = $NewPassword.ToPlainText() + } + } + } +} #endregion #region Jenkins Setup @@ -2094,7 +1039,9 @@ function Get-ChocoEnvironmentProperty { [switch]$AsPlainText ) begin { - $Content = Import-Clixml -Path "$env:SystemDrive\choco-setup\clixml\chocolatey-for-business.xml" + if (Test-Path "$env:SystemDrive\choco-setup\clixml\chocolatey-for-business.xml") { + $Content = Import-Clixml -Path "$env:SystemDrive\choco-setup\clixml\chocolatey-for-business.xml" + } } process { if ($Name) { @@ -2214,6 +1161,28 @@ function Set-JenkinsCertificate { Restart-Service Jenkins } } + +function Update-JenkinsJobParameters { + param( + [string]$JobsPath = "C:\ProgramData\Jenkins\.jenkins\jobs", + + [hashtable]$Replacement = @{} + ) + process { + foreach ($Job in Get-ChildItem $JobsPath -Filter config.xml -Recurse) { + Write-Verbose "Updating parameters in '$($Job.DirectoryName)'" + [xml]$Config = (Get-Content $Job.FullName) -replace "^\<\?xml version=['""]1\.1['""]","Accounts and Secrets - + - - - + + - + + + + @@ -180,7 +182,6 @@ function CopyToClipboard(id) -
NameUrlUsernamePasswordApiKey
NameUrlUsernamePassword
Chocolatey Central Management {{ ccm_fqdn }}:{{ ccm_port }} ccmadmin
{{ ccm_password | e }}
N/A
Nexus{{ nexus_fqdn }}:{{ nexus_port }}Nexus{{ nexus_fqdn }}:{{ nexus_port }} admin
{{ nexus_password | e }}
{{ lookup('file', 'credentials/nexus_apikey') | default('Unavailable') }}
{{ nexus_client_username }}
{{ nexus_client_password | e }}
{{ jenkins_fqdn }}:{{ jenkins_port }} admin
{{ jenkins_password | e }}
N/A


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

📝 Note

diff --git a/scripts/ClientSetup.ps1 b/scripts/ClientSetup.ps1 index ed1b176..3b0f6fa 100644 --- a/scripts/ClientSetup.ps1 +++ b/scripts/ClientSetup.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS -Completes client setup for a client machine to communicate with the C4B Server. +Completes client setup for a client machine to communicate with CCM. #> [CmdletBinding(DefaultParameterSetName = 'Default')] param( @@ -9,12 +9,11 @@ param( [Parameter()] [Alias('Url')] [string] - $RepositoryUrl = 'https://{{hostname}}:8443/repository/ChocolateyInternal/index.json', + $RepositoryUrl = 'https://{{hostname}}/repository/ChocolateyInternal/index.json', - # The credential necessary to access the internal Nexus repository. This can - # be ignored if Anonymous authentication is enabled. - # This parameter will be necessary if your C4B server is web-enabled. + # The credential used to access the internal Nexus repository. [Parameter(Mandatory)] + [Alias('Credential')] [pscredential] $RepositoryCredential, @@ -42,16 +41,18 @@ param( # Client salt value used to populate the centralManagementClientCommunicationSaltAdditivePassword # value in the Chocolatey config file [Parameter()] + [Alias('ClientSalt')] [string] $ClientCommunicationSalt, # Server salt value used to populate the centralManagementServiceCommunicationSaltAdditivePassword # value in the Chocolatey config file [Parameter()] + [Alias('ServerSalt')] [string] $ServiceCommunicationSalt, - #Install the Chocolatey Licensed Extension with right-click context menus available + # Install the Chocolatey Licensed Extension with right-click context menus available [Parameter()] [Switch] $IncludePackageTools, @@ -62,7 +63,7 @@ param( [Hashtable] $AdditionalConfiguration, - # Allows for the toggling of additonal features that is applied after the base configuration. + # Allows for the toggling of additional features that is applied after the base configuration. # Can override base configuration with this parameter [Parameter()] [Hashtable] @@ -82,8 +83,7 @@ param( Set-ExecutionPolicy Bypass -Scope Process -Force -$hostAddress = $RepositoryUrl.Split('/')[2] -$hostName = ($hostAddress -split ':')[0] +$hostName = ([uri]$RepositoryUrl).DnsSafeHost $params = @{ ChocolateyVersion = $ChocolateyVersion @@ -91,16 +91,19 @@ $params = @{ UseNativeUnzip = $true } -if (-not $IgnoreProxy) { - if ($ProxyUrl) { - $proxy = [System.Net.WebProxy]::new($ProxyUrl, $true <#bypass on local#>) - $params.Add('ProxyUrl', $ProxyUrl) - } +if (-not $IgnoreProxy -and $ProxyUrl) { + $Proxy = [System.Net.WebProxy]::new( + $ProxyUrl, + $true # Bypass Local Addresses + ) + $params.Add('ProxyUrl', $ProxyUrl) if ($ProxyCredential) { + $Proxy.Credentials = $ProxyCredential $params.Add('ProxyCredential', $ProxyCredential) - $proxy.Credentials = $ProxyCredential - + } elseif ($DefaultProxyCredential = [System.Net.CredentialCache]::DefaultCredentials) { + $Proxy.Credentials = $DefaultProxyCredential + $params.Add('ProxyCredential', $DefaultProxyCredential) } } @@ -114,23 +117,22 @@ $NupkgUrl = if (-not $ChocolateyVersion) { $QueryUrl = (($RepositoryUrl -replace '/index\.json$'), "v3/registration/Chocolatey/index.json") -join '/' $Result = $webClient.DownloadString($QueryUrl) | ConvertFrom-Json $Result.items.items[-1].packageContent -} -else { +} else { # Otherwise, assume the URL "$($RepositoryUrl -replace '/index\.json$')/v3/content/chocolatey/$($ChocolateyVersion)/chocolatey.$($ChocolateyVersion).nupkg" } +$webClient.Proxy = if ($Proxy -and -not $Proxy.IsBypassed($NupkgUrl)) {$Proxy} + # Download the NUPKG $NupkgPath = Join-Path $env:TEMP "$(New-Guid).zip" $webClient.DownloadFile($NupkgUrl, $NupkgPath) # Add Parameter for ChocolateyDownloadUrl, that is the NUPKG path $params.Add('ChocolateyDownloadUrl', $NupkgPath) - -# Get the script content -$script = $webClient.DownloadString("https://${hostAddress}/repository/choco-install/ChocolateyInstall.ps1") - -# Run the Chocolatey Install script with the parameters provided +$InstallScriptUrl = $RepositoryUrl -replace '\/repository\/(?.+)\/(index.json)?$', '/repository/choco-install/ChocolateyInstall.ps1' +$webClient.Proxy = if ($Proxy -and -not $Proxy.IsBypassed($InstallScriptUrl)) {$Proxy} +$script = $webClient.DownloadString($InstallScriptUrl) & ([scriptblock]::Create($script)) @params # If FIPS is enabled, configure Chocolatey to use FIPS compliant checksums @@ -146,23 +148,24 @@ choco config set commandExecutionTimeoutSeconds 14400 # Nexus NuGet V3 Compatibility choco feature disable --name="'usePackageRepositoryOptimizations'" -# Environment base Source configuration choco source add --name="'ChocolateyInternal'" --source="'$RepositoryUrl'" --allow-self-service --user="'$($RepositoryCredential.UserName)'" --password="'$($RepositoryCredential.GetNetworkCredential().Password)'" --priority=1 + choco source disable --name="'Chocolatey'" choco source disable --name="'chocolatey.licensed'" -choco upgrade chocolatey-license -y --source="'ChocolateyInternal'" -if (-not $IncludePackageTools) { - choco upgrade chocolatey.extension -y --params="'/NoContextMenu'" --source="'ChocolateyInternal'" --no-progress -} -else { - Write-Warning "IncludePackageTools was passed. Right-Click context menus will be available for installers, .nupkg, and .nuspec file types!" - choco upgrade chocolatey.extension -y --source="'ChocolateyInternal'" --no-progress -} -choco upgrade chocolateygui -y --source="'ChocolateyInternal'" --no-progress -choco upgrade chocolateygui.extension -y --source="'ChocolateyInternal'" --no-progress +choco upgrade chocolatey-license --confirm --source="'ChocolateyInternal'" +choco upgrade chocolatey.extension --confirm --source="'ChocolateyInternal'" --no-progress @( + if (-not $IncludePackageTools) { + '--params="/NoContextMenu"' + } else { + Write-Verbose "IncludePackageTools was passed. Right-Click context menus will be available for installers, .nupkg, and .nuspec file types!" + } +) -choco upgrade chocolatey-agent -y --source="'ChocolateyInternal'" +choco upgrade chocolateygui --confirm --source="'ChocolateyInternal'" --no-progress +choco upgrade chocolateygui.extension --confirm --source="'ChocolateyInternal'" --no-progress + +choco upgrade chocolatey-agent --confirm --source="'ChocolateyInternal'" # Chocolatey Package Upgrade Resilience choco feature enable --name="'excludeChocolateyPackagesDuringUpgradeAll'" @@ -188,20 +191,19 @@ if ($ServiceCommunicationSalt) { choco feature enable --name="'useChocolateyCentralManagement'" choco feature enable --name="'useChocolateyCentralManagementDeployments'" - if ($AdditionalConfiguration -or $AdditionalFeatures -or $AdditionalSources -or $AdditionalPackages) { - Write-Host "Applying user supplied configuration" -ForegroundColor Cyan + Write-Host "Applying user supplied configuration" } -# How we call choco from here changes as we need to be more dynamic with thingsii . + if ($AdditionalConfiguration) { - <# +<# We expect to pass in a hashtable with configuration information with the following shape: @{ - Name = BackgroundServiceAllowedCommands - Value = 'install,upgrade,uninstall' + BackgroundServiceAllowedCommands = 'install,upgrade,uninstall' + commandExecutionTimeoutSeconds = 6000 } - #> +#> $AdditionalConfiguration.GetEnumerator() | ForEach-Object { $Config = [System.Collections.Generic.list[string]]::new() @@ -215,15 +217,14 @@ if ($AdditionalConfiguration) { } if ($AdditionalFeatures) { - <# +<# We expect to pass in feature information as a hashtable with the following shape: @{ useBackgroundservice = 'Enabled' } - #> +#> $AdditionalFeatures.GetEnumerator() | ForEach-Object { - $Feature = [System.Collections.Generic.list[string]]::new() $Feature.Add('feature') @@ -240,13 +241,12 @@ if ($AdditionalFeatures) { } if ($AdditionalSources) { - - <# - We expect a user to pass in a hashtable with source information with the folllowing shape: +<# + We expect a user to pass in a hashtable with source information with the following shape: @{ Name = 'MySource' Source = 'https://nexus.fabrikam.com/repository/MyChocolateySource' - #Optional items + # Optional items Credentials = $MySourceCredential AllowSelfService = $true AdminOnly = $true @@ -256,7 +256,7 @@ if ($AdditionalSources) { CertificatePassword = 's0mepa$$' } #> - Foreach ($Source in $AdditionalSources) { + foreach ($Source in $AdditionalSources) { $SourceSplat = [System.Collections.Generic.List[string]]::new() # Required items $SourceSplat.Add('source') @@ -284,8 +284,7 @@ if ($AdditionalSources) { } if ($AdditionalPackages) { - - <# +<# We expect to pass in a hashtable with package information with the following shape: @{ @@ -294,9 +293,8 @@ if ($AdditionalPackages) { Version = 123.4.56 Pin = $true } - #> +#> foreach ($package in $AdditionalPackages.GetEnumerator()) { - $PackageSplat = [System.Collections.Generic.list[string]]::new() $PackageSplat.add('install') $PackageSplat.add($package['Id']) diff --git a/scripts/Create-ChocoLicensePkg.ps1 b/scripts/Create-ChocoLicensePkg.ps1 index 3cc11f4..395ecbc 100644 --- a/scripts/Create-ChocoLicensePkg.ps1 +++ b/scripts/Create-ChocoLicensePkg.ps1 @@ -45,11 +45,6 @@ $PackagingFolder = "$env:SystemDrive\choco-setup\packaging" $licensePackageFolder = "$PackagingFolder\$LicensePackageId" $licensePackageNuspec = "$licensePackageFolder\$LicensePackageId.nuspec" -Write-Warning "Prior to running this, please ensure you've updated the license file first at $LicensePath" -Write-Warning "This script will OVERWRITE any existing license file you might have placed in '$licensePackageFolder'" -& choco.exe | Out-String -Stream | Write-Host -Write-Warning "If there is is a note about invalid license above, you're going to run into issues." - # Get license expiration date and node count [xml]$licenseXml = Get-Content -Path $LicensePath $licenseExpiration = [datetimeoffset]::Parse("$($licenseXml.SelectSingleNode('/license').expiration) +0") @@ -67,17 +62,17 @@ if (-not $LicensePackageVersion) { } # Ensure the packaging folder exists -Write-Host "Generating package/packaging folders at '$PackagingFolder'" +Write-Verbose "Generating package/packaging folders at '$PackagingFolder'" New-Item $PackagingFolder -ItemType Directory -Force | Out-Null New-Item $PackagesPath -ItemType Directory -Force | Out-Null # Create a new package -Write-Host "Creating package named '$LicensePackageId'" +Write-Verbose "Creating package named '$LicensePackageId'" New-Item $licensePackageFolder -ItemType Directory -Force | Out-Null New-Item "$licensePackageFolder\tools" -ItemType Directory -Force | Out-Null # Set the installation script -Write-Host "Setting install and uninstall scripts..." +Write-Verbose "Setting install and uninstall scripts..." @' $ErrorActionPreference = 'Stop' $toolsDir = Split-Path -Parent $MyInvocation.MyCommand.Definition @@ -95,12 +90,11 @@ Write-Host "Setting install and uninstall scripts..." '@ | Set-Content -Path "$licensePackageFolder\tools\chocolateyUninstall.ps1" -Encoding UTF8 -Force # Copy the license to the package directory -Write-Host "Copying license to package from '$LicensePath' to package location." -Write-Warning "This will overwrite the file in the package, even if that's where you placed the updated license." +Write-Verbose "Copying license to package from '$LicensePath' to package location." Copy-Item -Path $LicensePath -Destination "$licensePackageFolder\tools\chocolatey.license.xml" -Force # Set the nuspec -Write-Host "Setting nuspec..." +Write-Verbose "Setting nuspec..." @" @@ -133,9 +127,9 @@ If items are installed in any other order, it could have strange effects or fail "@.Trim() | Set-Content -Path "$licensePackageNuspec" -Encoding UTF8 -Force # Package up everything -Write-Host "Creating license package..." -choco pack $licensePackageNuspec --output-directory="$PackagesPath" -Write-Host "Package has been created and is ready at $PackagesPath" +Write-Verbose "Creating license package..." +Invoke-Choco pack $licensePackageNuspec --output-directory="$PackagesPath" +Write-Verbose "Package has been created and is ready at $PackagesPath" -Write-Host "Installing newly created package on this machine, making updates to license easier in the future, if pushed from another location later." -choco upgrade chocolatey-license -y --source="'$PackagesPath'" +Write-Verbose "Installing newly created package on this machine, making updates to license easier in the future, if pushed from another location later." +Invoke-Choco upgrade chocolatey-license -y --source="'$PackagesPath'" diff --git a/scripts/Register-C4bEndpoint.ps1 b/scripts/Register-C4bEndpoint.ps1 index 627c280..650f63f 100644 --- a/scripts/Register-C4bEndpoint.ps1 +++ b/scripts/Register-C4bEndpoint.ps1 @@ -76,6 +76,7 @@ Param( [Parameter()] [String] $ProxyUrl, + # The credentials, if required, to connect to the proxy server. [Parameter()] [PSCredential] @@ -148,7 +149,7 @@ begin { } end { - # If we use a Self-Signed certificate, we need to explicity trust it + # If we use a Self-Signed certificate, we need to explicitly trust it if ($TrustCertificate) { Invoke-Expression ($downloader.DownloadString("http://$($Fqdn):80/Import-ChocoServerCertificate.ps1")) } diff --git a/tests/jenkins.tests.ps1 b/tests/jenkins.tests.ps1 index 5b0a13b..acea70e 100644 --- a/tests/jenkins.tests.ps1 +++ b/tests/jenkins.tests.ps1 @@ -31,10 +31,6 @@ Describe "Jenkins Configuration" { $Scripts = (Get-ChildItem 'C:\Scripts' -Recurse -Filter *.ps1).Name } - It "ConvertTo-ChocoObject is present" { - 'ConvertTo-ChocoObject.ps1' -in $Scripts | Should -Be $true - } - It "Get-UpdatedPackage.ps1 is present" { 'Get-UpdatedPackage.ps1' -in $Scripts | Should -Be $true } diff --git a/tests/nexus.tests.ps1 b/tests/nexus.tests.ps1 index df30c15..aa9f991 100644 --- a/tests/nexus.tests.ps1 +++ b/tests/nexus.tests.ps1 @@ -57,8 +57,7 @@ Describe "Nexus Configuration" { Context "Repository Configuration" { BeforeAll { - $password = (Get-Content 'C:\ProgramData\sonatype-work\nexus3\admin.password') | ConvertTo-SecureString -AsPlainText -Force - $credential = [System.Management.Automation.PSCredential]::new('admin',$password) + $credential = Get-ChocoEnvironmentProperty NexusCredential . "C:\choco-setup\files\scripts\Get-Helpers.ps1" $null = Connect-NexusServer -Hostname $Fqdn -Credential $credential -UseSSL diff --git a/tests/server.tests.ps1 b/tests/server.tests.ps1 index e05e667..a695d7a 100644 --- a/tests/server.tests.ps1 +++ b/tests/server.tests.ps1 @@ -45,14 +45,13 @@ Describe "Server Integrity" { $Logs = Get-ChildItem C:\choco-setup\logs -Recurse -Filter *.txt } - It " log file was created during installation" -Foreach @( - @{File = 'Set-SslCertificate'} - @{File = 'Start-C4bCcmSetup'} - @{File = 'Start-C4bJenkinsSetup'} - @{File = 'Start-C4bNexusSetup'} - @{File = 'Start-C4bSetup'} + It "<_> log file was created during installation" -ForEach @( + 'Start-C4bCcmSetup' + 'Start-C4bJenkinsSetup' + 'Start-C4bNexusSetup' + 'Initialize-C4bSetup' ) { - Test-Path "C:\choco-setup\logs\$($_.File)*.txt" | Should -Be $true + Test-Path "C:\choco-setup\logs\$($_)*.txt" | Should -Be $true } } }