From a6038928cb6ca4fb503535e111b12d96f8f2a324 Mon Sep 17 00:00:00 2001 From: James Ruskin Date: Wed, 18 Sep 2024 11:28:01 +0100 Subject: [PATCH] (#245) Moves Helper Functions into Module This is good practice for work going forward, and begins to enable some of the later stage work we want to do to unify our environment setup(s). sq --- .github/workflows/test.yml | 2 +- OfflineInstallPreparation.ps1 | 2 +- Set-SslSecurity.ps1 | 10 +- Start-C4bCcmSetup.ps1 | 4 +- Start-C4bJenkinsSetup.ps1 | 4 +- Start-C4bNexusSetup.ps1 | 4 +- Start-C4bSetup.ps1 | 10 +- Start-C4bVerification.ps1 | 7 +- modules/C4B-Environment/C4B-Environment.psd1 | 132 + modules/C4B-Environment/C4B-Environment.psm1 | 2323 +++++++++++++++++ .../C4B-Environment}/ReadmeTemplate.html.j2 | 0 scripts/Get-Helpers.ps1 | 2322 +--------------- tests/nexus.tests.ps1 | 2 +- 13 files changed, 2475 insertions(+), 2347 deletions(-) create mode 100644 modules/C4B-Environment/C4B-Environment.psd1 create mode 100644 modules/C4B-Environment/C4B-Environment.psm1 rename {scripts => modules/C4B-Environment}/ReadmeTemplate.html.j2 (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 35983fa..9890577 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -331,7 +331,7 @@ jobs: param( $Path ) - . C:\choco-setup\files\scripts\Get-Helpers.ps1 + Import-Module C:\choco-setup\files\modules\C4B-Environment $configuration = New-PesterConfiguration @{ Run = @{ Container = New-PesterContainer -Path $Path -Data @{ Fqdn = $using:CertDetails.FQDN } diff --git a/OfflineInstallPreparation.ps1 b/OfflineInstallPreparation.ps1 index 10e71ff..b79292a 100644 --- a/OfflineInstallPreparation.ps1 +++ b/OfflineInstallPreparation.ps1 @@ -60,7 +60,7 @@ $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" $LicensePath = Convert-Path $LicensePath -. $PSScriptRoot\scripts\Get-Helpers.ps1 +Import-Module $PSScriptRoot\modules\C4B-Environment $ChocoInstallScript = Join-Path $PSScriptRoot "scripts\ChocolateyInstall.ps1" if (-not (Test-Path $ChocoInstallScript)) { diff --git a/Set-SslSecurity.ps1 b/Set-SslSecurity.ps1 index 81356b4..6ad3997 100644 --- a/Set-SslSecurity.ps1 +++ b/Set-SslSecurity.ps1 @@ -1,3 +1,4 @@ +#requires -modules C4B-Environment using namespace System.Net.Sockets using namespace System.Net.Security using namespace System.Security.Cryptography.X509Certificates @@ -75,10 +76,7 @@ process { $DefaultEap = $ErrorActionPreference $ErrorActionPreference = 'Stop' Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Set-SslCertificate-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" - - # Dot-source helper functions - $ScriptDir = Join-Path $PSScriptRoot "scripts" - . $ScriptDir\Get-Helpers.ps1 + # Collect current certificate configuration $Certificate = if ($Subject) { Get-Certificate -Subject $Subject @@ -149,7 +147,7 @@ process { Connect-NexusServer -Hostname $SubjectWithoutCn -Credential $Credential -UseSSL # Push ClientSetup.ps1 to raw repo - $ClientScript = "$ScriptDir\ClientSetup.ps1" + $ClientScript = "$PSScriptRoot\scripts\ClientSetup.ps1" (Get-Content -Path $ClientScript) -replace "{{hostname}}", $SubjectWithoutCn | Set-Content -Path $ClientScript New-NexusRawComponent -RepositoryName 'choco-install' -File $ClientScript @@ -255,7 +253,7 @@ process { } # Generate Register-C4bEndpoint.ps1 - $EndpointScript = "$ScriptDir\Register-C4bEndpoint.ps1" + $EndpointScript = "$PSScriptRoot\scripts\Register-C4bEndpoint.ps1" if ($Hardened) { diff --git a/Start-C4bCcmSetup.ps1 b/Start-C4bCcmSetup.ps1 index 70ceb3a..b0b81fe 100644 --- a/Start-C4bCcmSetup.ps1 +++ b/Start-C4bCcmSetup.ps1 @@ -1,3 +1,4 @@ +#requires -modules C4B-Environment <# .SYNOPSIS C4B Quick-Start Guide CCM setup script @@ -26,9 +27,6 @@ process { $ErrorActionPreference = 'Stop' Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Start-C4bCcmSetup-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" - # Dot-source helper functions - . .\scripts\Get-Helpers.ps1 - $Packages = (Get-Content $PSScriptRoot\files\chocolatey.json | ConvertFrom-Json).packages Set-ChocoEnvironmentProperty -Name DatabaseUser -Value $DatabaseCredential diff --git a/Start-C4bJenkinsSetup.ps1 b/Start-C4bJenkinsSetup.ps1 index f643827..c60ec99 100644 --- a/Start-C4bJenkinsSetup.ps1 +++ b/Start-C4bJenkinsSetup.ps1 @@ -1,3 +1,4 @@ +#requires -Modules C4B-Environment <# .SYNOPSIS C4B Quick-Start Guide Jenkins setup script @@ -24,9 +25,6 @@ process { $ErrorActionPreference = 'Stop' Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Start-C4bJenkinsSetup-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" - # Dot-source helper functions - . .\scripts\Get-Helpers.ps1 - # Install temurin21jre to meet JRE>11 dependency of Jenkins $chocoArgs = @('install', 'temurin21jre', '-y', '--no-progress', "--params='/ADDLOCAL=FeatureJavaHome'") & choco @chocoArgs diff --git a/Start-C4bNexusSetup.ps1 b/Start-C4bNexusSetup.ps1 index 259e48c..0e20cb9 100644 --- a/Start-C4bNexusSetup.ps1 +++ b/Start-C4bNexusSetup.ps1 @@ -1,3 +1,4 @@ +#requires -Modules C4B-Environment <# .SYNOPSIS C4B Quick-Start Guide Nexus setup script @@ -25,9 +26,6 @@ process { $ErrorActionPreference = 'Stop' Start-Transcript -Path "$env:SystemDrive\choco-setup\logs\Start-C4bNexusSetup-$(Get-Date -Format 'yyyyMMdd-HHmmss').txt" - # Dot-source helper functions - . .\scripts\Get-Helpers.ps1 - $Packages = (Get-Content $PSScriptRoot\files\chocolatey.json | ConvertFrom-Json).packages # Install base nexus-repository package diff --git a/Start-C4bSetup.ps1 b/Start-C4bSetup.ps1 index 6ea8188..2688533 100644 --- a/Start-C4bSetup.ps1 +++ b/Start-C4bSetup.ps1 @@ -119,8 +119,14 @@ try { } } - # Import Helper Functions - . $FilesDir\scripts\Get-Helpers.ps1 + # Add the Module Path and Import Helper Functions + if (-not (Get-Module C4B-Environment -ListAvailable)) { + if ($env:PSModulePath.Split(';') -notcontains "$PSScriptRoot\modules") { + [Environment]::SetEnvironmentVariable("PSModulePath", "$env:PSModulePath;$PSScriptRoot\modules" ,"Machine") + $env:PSModulePath = [Environment]::GetEnvironmentVariables("Machine").PSModulePath + } + } + Import-Module C4B-Environment -Verbose:$false # Downloading all CCM setup packages below Write-Host "Downloading missing nupkg files to $($PkgsDir)." -ForegroundColor Green diff --git a/Start-C4bVerification.ps1 b/Start-C4bVerification.ps1 index f3bf82d..214fe60 100644 --- a/Start-C4bVerification.ps1 +++ b/Start-C4bVerification.ps1 @@ -1,16 +1,11 @@ +#requires -modules C4B-Environment [CmdletBinding()] Param( [Parameter(Mandatory)] [String] $Fqdn ) - process { - #Load helper functions in scope for tests - $HelperPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Definition) -ChildPath 'scripts' - $Helpers = Join-Path $HelperPath -ChildPath 'Get-Helpers.ps1' - . $Helpers - if (-not (Get-Module Pester -ListAvailable).Where{$_.Version -gt "5.0"}) { Write-Host "Installing Pester 5 to run validation tests" $chocoArgs = @('install', 'pester', '-y', '--no-progress', '--source="https://community.chocolatey.org/api/v2/"') diff --git a/modules/C4B-Environment/C4B-Environment.psd1 b/modules/C4B-Environment/C4B-Environment.psd1 new file mode 100644 index 0000000..3c1a18b --- /dev/null +++ b/modules/C4B-Environment/C4B-Environment.psd1 @@ -0,0 +1,132 @@ +# +# Module manifest for module 'c4b-helpers' +# +# Generated by: james +# +# Generated on: 18/09/2024 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'C4B-Environment.psm1' + +# Version number of this module. +ModuleVersion = '0.0.1' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '29a70562-5048-4ab9-9654-1bd5e962b1c5' + +# Author of this module +Author = 'james' + +# Company or vendor of this module +CompanyName = 'Chocolatey Software' + +# Copyright statement for this module +Copyright = '(c) james. All rights reserved.' + +# Description of the functionality provided by this module +# Description = '' + +# Minimum version of the PowerShell engine required by this module +# PowerShellVersion = '' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = '*' + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = '*' + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = '*' + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + # ProjectUri = '' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/modules/C4B-Environment/C4B-Environment.psm1 b/modules/C4B-Environment/C4B-Environment.psm1 new file mode 100644 index 0000000..94c17c5 --- /dev/null +++ b/modules/C4B-Environment/C4B-Environment.psm1 @@ -0,0 +1,2323 @@ +# Helper Functions for the various QSG scripts +function Invoke-Choco { + [CmdletBinding()] + [Alias('choco')] + param( + [Parameter(Position=0)] + [string]$Command, + + [Parameter(Position=1, ValueFromRemainingArguments)] + [string[]]$Arguments, + + [int[]]$ValidExitCodes = @(0) + ) + + if ($Command -eq 'Install' -and $Arguments -notmatch '\b-(y|-confirm)\b') { + $Arguments += '--confirm' + } + + if ($Arguments -notmatch '\b-(r|-limitoutput|-limit-output)\b') { + $Arguments += '--limit-output' + } + + $chocoPath = if ($CommandPath = Get-Command choco.exe -ErrorAction SilentlyContinue) { + $CommandPath.Source + } elseif ($env:ChocolateyInstall) { + Join-Path $env:ChocolateyInstall "choco.exe" + } elseif (Test-Path C:\ProgramData\chocolatey\choco.exe) { + "C:\ProgramData\chocolatey\choco.exe" + } else { + Write-Error "Could not find 'choco.exe' - unexpected behaviour is expected!" + "choco.exe" + } + + & $chocoPath $Command $Arguments | Tee-Object -Variable Result | Where-Object {$_} | ForEach-Object { + Write-Information -MessageData $_ -Tags Choco + } + + if ($LASTEXITCODE -notin $ValidExitCodes) { + Write-Error -Message "$($Result[-5..-1] -join "`n")" -TargetObject "choco $Command $Arguments" + } +} + +Update-TypeData -TypeName SecureString -MemberType ScriptMethod -MemberName ToPlainText -Force -Value { + [System.Net.NetworkCredential]::new("TempCredential", $this).Password +} + +#region Package functions (OfflineInstallPreparation.ps1) +if (-not ("System.IO.Compression.ZipArchive" -as [type])) { + Add-Type -Assembly 'System.IO.Compression' +} + +function Find-FileInArchive { + <# + .Synopsis + Finds files with a name matching a pattern in an archive. + .Example + Find-FileInArchive -Path "C:\Archive.zip" -like "tools/files/*-x86.exe" + .Example + Find-FileInArchive -Path $Nupkg -match "tools/files/dotnetcore-sdk-(?\d+\.\d+\.\d+)-win-x86\.exe(\.ignore)?" + .Notes + Please be aware that this matches against the full name of the file, not just the file name. + Though given that, you can easily write something to match the file name. + #> + [CmdletBinding(DefaultParameterSetName = "match")] + param( + # Path to the archive + [Parameter(Mandatory)] + [string]$Path, + + # Pattern to match with regex + [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = "match")] + [string]$match, + + # Pattern to match with basic globbing + [Parameter(Mandatory, ParameterSetName = "like")] + [string]$like + ) + begin { + while (-not $Zip -and $AccessRetries++ -lt 3) { + try { + $Stream = [IO.FileStream]::new($Path, [IO.FileMode]::Open) + $Zip = [IO.Compression.ZipArchive]::new($Stream, [IO.Compression.ZipArchiveMode]::Read) + } catch [System.IO.IOException] { + if ($AccessRetries -ge 3) { + Write-Error -Message "Accessing '$Path' failed after $AccessRetries attempts." -TargetObject $Path + } else { + Write-Information "Could not access '$Path', retrying..." + Start-Sleep -Milliseconds 500 + } + } + } + } + process { + if ($Zip) { + # Improve "security"? + $WhereBlock = [ScriptBlock]::Create("`$_.FullName -$($PSCmdlet.ParameterSetName) '$(Get-Variable -Name $PSCmdlet.ParameterSetName -ValueOnly)'") + $Zip.Entries | Where-Object -FilterScript $WhereBlock + } + } + end { + if ($Zip) { + $Zip.Dispose() + } + if ($Stream) { + $Stream.Close() + $Stream.Dispose() + } + } +} + +function Get-FileContentInArchive { + <# + .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(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 + } + } +} + +function New-NexusRole { + <# + .SYNOPSIS + Creates a new Nexus Role + + .DESCRIPTION + Creates a new Nexus Role + + .PARAMETER Id + The ID of the role + + .PARAMETER Name + The friendly name of the role + + .PARAMETER Description + A description of the role + + .PARAMETER Privileges + Included privileges for the role + + .PARAMETER Roles + Included nested roles + + .EXAMPLE + New-NexusRole -Id SamepleRole + + .EXAMPLE + New-NexusRole -Id SampleRole -Description "A sample role" -Privileges nx-all + + .NOTES + + #> + [CmdletBinding(HelpUri = 'https://steviecoaster.dev/NexuShell/Security/Roles/New-NexusRole/')] + Param( + [Parameter(Mandatory)] + [String] + $Id, + + [Parameter(Mandatory)] + [String] + $Name, + + [Parameter()] + [String] + $Description, + + [Parameter(Mandatory)] + [String[]] + $Privileges, + + [Parameter()] + [String[]] + $Roles + ) + + begin { + if (-not $header) { + throw 'Not connected to Nexus server! Run Connect-NexusServer first.' + } + } + + process { + + $urislug = '/service/rest/v1/security/roles' + $Body = @{ + + id = $Id + name = $Name + description = $Description + privileges = @($Privileges) + roles = $Roles + + } + + Invoke-Nexus -Urislug $urislug -Body $Body -Method POST | Foreach-Object { + [PSCustomobject]@{ + Id = $_.id + Name = $_.name + Description = $_.description + Privileges = $_.privileges + Roles = $_.roles + } + } + + } +} + +function Set-NexusAnonymousAuth { + <# + .SYNOPSIS + Turns Anonymous Authentication on or off in Nexus + + .DESCRIPTION + Turns Anonymous Authentication on or off in Nexus + + .PARAMETER Enabled + Turns on Anonymous Auth + + .PARAMETER Disabled + Turns off Anonymous Auth + + .EXAMPLE + Set-NexusAnonymousAuth -Enabled + #> + [CmdletBinding(HelpUri = 'https://steviecoaster.dev/NexuShell/Set-NexusAnonymousAuth/')] + Param( + [Parameter()] + [Switch] + $Enabled, + + [Parameter()] + [Switch] + $Disabled + ) + + begin { + + if (-not $header) { + throw "Not connected to Nexus server! Run Connect-NexusServer first." + } + + $urislug = "/service/rest/v1/security/anonymous" + } + + process { + + Switch ($true) { + + $Enabled { + $Body = @{ + enabled = $true + userId = 'anonymous' + realmName = 'NexusAuthorizingRealm' + } + + Invoke-Nexus -UriSlug $urislug -Body $Body -Method 'PUT' + } + + $Disabled { + $Body = @{ + enabled = $false + userId = 'anonymous' + realmName = 'NexusAuthorizingRealm' + } + + Invoke-Nexus -UriSlug $urislug -Body $Body -Method 'PUT' + + } + } + } +} + +#endregion + +#region SSL functions (Set-SslSecurity.ps1) + +function Get-Certificate { + [CmdletBinding()] + param( + [Parameter()] + [string] + $Thumbprint, + + [Parameter()] + [string] + $Subject + ) + + $filter = if ($Thumbprint) { + { $_.Thumbprint -eq $Thumbprint } + } + else { + { $_.Subject -like "CN=$Subject" } + } + + $cert = Get-ChildItem -Path Cert:\LocalMachine\My, Cert:\LocalMachine\TrustedPeople | + Where-Object $filter -ErrorAction Stop | + Select-Object -First 1 + + if ($null -eq $cert) { + throw "Certificate either not found, or other issue arose." + } + else { + Write-Host "Certification validation passed" -ForegroundColor Green + $cert + } +} + +function Copy-CertToStore { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [System.Security.Cryptography.X509Certificates.X509Certificate2] + $Certificate + ) + + $location = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine + $trustedCertStore = [System.Security.Cryptography.X509Certificates.X509Store]::new('TrustedPeople', $location) + + try { + $trustedCertStore.Open('ReadWrite') + $trustedCertStore.Add($Certificate) + } + finally { + $trustedCertStore.Close() + $trustedCertStore.Dispose() + } +} + +function Get-RemoteCertificate { + param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$ComputerName, + + [Parameter(Position = 1)] + [UInt16]$Port = 8443 + ) + + $tcpClient = New-Object System.Net.Sockets.TcpClient($ComputerName, $Port) + $sslProtocolType = [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 + try { + $tlsClient = New-Object System.Net.Security.SslStream($tcpClient.GetStream(), 'false', $callback) + $tlsClient.AuthenticateAsClient($ComputerName, $null, $sslProtocolType, $false) + + return $tlsClient.RemoteCertificate -as [System.Security.Cryptography.X509Certificates.X509Certificate2] + } + finally { + if ($tlsClient -is [IDisposable]) { + $tlsClient.Dispose() + } + + $tcpClient.Dispose() + } +} + +function Set-NexusCert { + [CmdletBinding()] + param( + # The thumbprint of the certificate to configure Nexus to use, from the LocalMachine\TrustedPeople store. + [Parameter(Mandatory)] + $Thumbprint, + + # The port to set Nexus to use for https. + $Port = 8443 + ) + + $KeyTool = "C:\ProgramData\nexus\jre\bin\keytool.exe" + $KeyStorePath = 'C:\ProgramData\nexus\etc\ssl\keystore.jks' + $KeystoreCredential = [System.Net.NetworkCredential]::new( + "Keystore", + (New-ServicePassword) + ) + $TempCertPath = Join-Path $env:TEMP "$(New-Guid).pfx" + + try { + # Temporarily export the certificate as a PFX + Get-ChildItem Cert:\LocalMachine\TrustedPeople\ | Where-Object { $_.Thumbprint -eq $Thumbprint } | Sort-Object | Select-Object -First 1 | Export-PfxCertificate -FilePath $TempCertPath -Password $KeystoreCredential.SecurePassword + # TODO: Is this the right place for this? # Get-ChildItem -Path $TempCertPath | Import-PfxCertificate -CertStoreLocation Cert:\LocalMachine\My -Exportable -Password $KeystoreCredential.SecurePassword + + if (Test-Path $KeyStorePath) { + Remove-Item $KeyStorePath -Force + } + + # Using a job to hide improper non-output streams + $Job = Start-Job { + $string = ($using:KeystoreCredential.Password | & $using:KeyTool -list -v -keystore $using:TempCertPath -J"-Duser.language=en") -match '^Alias.*' + $currentAlias = ($string -split ':')[1].Trim() + & $using:KeyTool -importkeystore -srckeystore $using:TempCertPath -srcstoretype PKCS12 -srcstorepass $using:KeystoreCredential.Password -destkeystore $using:KeyStorePath -deststoretype JKS -alias $currentAlias -destalias jetty -deststorepass $using:KeystoreCredential.Password + & $using:KeyTool -keypasswd -keystore $using:KeyStorePath -alias jetty -storepass $using:KeystoreCredential.Password -keypass $using:KeystoreCredential.Password -new $using:KeystoreCredential.Password + } | Wait-Job + if ($Job.State -eq 'Failed') { + $Job | Receive-Job + } else { + $Job | Remove-Job + } + } finally { + if (Test-Path $TempCertPath) { + Remove-Item $TempCertPath -Force + } + } + + # Update the Nexus configuration + $xmlPath = 'C:\ProgramData\nexus\etc\jetty\jetty-https.xml' + [xml]$xml = Get-Content -Path 'C:\ProgramData\nexus\etc\jetty\jetty-https.xml' + foreach ($entry in $xml.Configure.New.Where{ $_.id -match 'ssl' }.Set.Where{ $_.name -match 'password' }) { + $entry.InnerText = $KeystoreCredential.Password + } + + $xml.Save($xmlPath) + + $configPath = "C:\ProgramData\sonatype-work\nexus3\etc\nexus.properties" + + # Remove existing ssl config from the configuration + (Get-Content $configPath) | Where-Object {$_ -notmatch "application-port-ssl="} | Set-Content $configPath + + # Ensure each line is added to the configuration + @( + 'jetty.https.stsMaxAge=-1' + "application-port-ssl=$Port" + 'nexus-args=${jetty.etc}/jetty.xml,${jetty.etc}/jetty-https.xml,${jetty.etc}/jetty-requestlog.xml' + ) | ForEach-Object { + if ((Get-Content -Raw $configPath) -notmatch [regex]::Escape($_)) { + $_ | Add-Content -Path $configPath + } + } + + if ((Get-Service Nexus).Status -eq 'Running') { + Restart-Service Nexus + } +} + +function Test-SelfSignedCertificate { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline = $true)] + $Certificate = (Get-ChildItem -Path Cert:LocalMachine\My | Where-Object { $_.FriendlyName -eq $SubjectWithoutCn }) + ) + + process { + + if ($Certificate.Subject -eq $Certificate.Issuer) { + return $true + } + else { + return $false + } + + } + +} + +#endregion + +#region CCM functions (Start-C4bCcmSetup.ps1) +function Add-DatabaseUserAndRoles { + param( + [parameter(Mandatory = $true)][string] $Username, + [parameter(Mandatory = $true)][string] $DatabaseName, + [parameter(Mandatory = $false)][string] $DatabaseServer = 'localhost\SQLEXPRESS', + [parameter(Mandatory = $false)] $DatabaseRoles = @('db_datareader'), + [parameter(Mandatory = $false)][string] $DatabaseServerPermissionsOptions = 'Trusted_Connection=true;', + [parameter(Mandatory = $false)][switch] $CreateSqlUser, + [parameter(Mandatory = $false)][string] $SqlUserPw + ) + + $LoginOptions = "FROM WINDOWS WITH DEFAULT_DATABASE=[$DatabaseName]" + if ($CreateSqlUser) { + $LoginOptions = "WITH PASSWORD='$SqlUserPw', DEFAULT_DATABASE=[$DatabaseName], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF" + } + + $addUserSQLCommand = @" +USE [master] +IF EXISTS(SELECT * FROM msdb.sys.syslogins WHERE UPPER([name]) = UPPER('$Username')) +BEGIN +DROP LOGIN [$Username] +END + +CREATE LOGIN [$Username] $LoginOptions + +USE [$DatabaseName] +IF EXISTS(SELECT * FROM sys.sysusers WHERE UPPER([name]) = UPPER('$Username')) +BEGIN +DROP USER [$Username] +END + +CREATE USER [$Username] FOR LOGIN [$Username] + +"@ + + foreach ($DatabaseRole in $DatabaseRoles) { + $addUserSQLCommand += @" +ALTER ROLE [$DatabaseRole] ADD MEMBER [$Username] +"@ + } + + Write-Host "Adding $UserName to $DatabaseName with the following permissions: $($DatabaseRoles -Join ', ')" + Write-Debug "running the following: \n $addUserSQLCommand" + $Connection = New-Object System.Data.SQLClient.SQLConnection + $Connection.ConnectionString = "server='$DatabaseServer';database='master';$DatabaseServerPermissionsOptions" + $Connection.Open() + $Command = New-Object System.Data.SQLClient.SQLCommand + $Command.CommandText = $addUserSQLCommand + $Command.Connection = $Connection + $null = $Command.ExecuteNonQuery() + $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 + Get-Process chocolateysoftware.chocolateymanagement.web* | Stop-Process -ErrorAction SilentlyContinue -Force +} + +function Remove-CcmBinding { + [CmdletBinding()] + param() + + process { + Write-Verbose "Removing existing bindings" + netsh http delete sslcert ipport=0.0.0.0:443 + } +} + +function New-CcmBinding { + [CmdletBinding()] + param( + [string]$Thumbprint + ) + 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" + Get-WebBinding -Name ChocolateyCentralManagement | Remove-WebBinding + New-WebBinding -Name ChocolateyCentralManagement -Protocol https -Port 443 -SslFlags 0 -IpAddress '*' +} + +function Start-CcmService { + try { + Start-Service chocolatey-central-management -ErrorAction Stop + } + catch { + #Try again... + Start-Service chocolatey-central-management -ErrorAction SilentlyContinue + } + finally { + if ((Get-Service chocolatey-central-management).Status -ne 'Running') { + Write-Warning "Unable to start Chocolatey Central Management service, please start manually in Services.msc" + } + } + +} + +function Set-CcmCertificate { + [CmdletBinding()] + Param( + [Parameter(Mandatory)] + [String] + $CertificateThumbprint + ) + + process { + Stop-Service chocolatey-central-management + $jsonData = Get-Content $env:ChocolateyInstall\lib\chocolatey-management-service\tools\service\appsettings.json | ConvertFrom-Json + $jsonData.CertificateThumbprint = $CertificateThumbprint + $jsonData | ConvertTo-Json | Set-Content $env:chocolateyInstall\lib\chocolatey-management-service\tools\service\appsettings.json + Start-Service chocolatey-central-management + } +} + +#endregion + +#region Jenkins Setup + +# Function to generate Jenkins password +function New-ServicePassword { + <# + .Synopsis + Generates and returns a suitably secure password suited for support calls + #> + [CmdletBinding()] + [OutputType([System.Security.SecureString])] + param( + [ValidateRange(1,128)] + [int]$Length = 64, + + [char[]]$AvailableCharacters = @( + # Specifically excluding $, `, ;, #, etc such that pasting + # passwords into support scripts will be more predictable. + "!%()*+,-./<=>?@[\]^_" + 48..57 # 0-9 + 65..90 # A-Z + 97..122 # a-z + ).ForEach{[char[]]$_} + ) + end { + $NewPassword = [System.Security.SecureString]::new() + + while ($NewPassword.Length -lt $Length) { + $NewPassword.AppendChar(($AvailableCharacters | Get-Random)) + } + + $NewPassword + } +} + +function Get-BcryptDll { + <# + .Synopsis + Finds the Bcrypt DLL if present, or downloads it if missing. Returns the full path to the DLL. + .Example + $BCryptDllPath = Get-BcryptDll + .Example + $BCryptDllPath = Get-BcryptDll -DestinationPath ~\Downloads + #> + [CmdletBinding()] + [OutputType([string])] + param( + # The path to find the DLL within, or extract the DLL to if unfound. + [Parameter(Position = 0)] + [string]$DestinationPath = (Join-Path $PSScriptRoot "bcrypt.net.0.1.0") + ) + end { + if (-not (Test-Path $DestinationPath)) { + $null = New-Item -Path $DestinationPath -ItemType Directory -Force + } + $ZipPath = Join-Path $env:TEMP 'bcrypt.net.0.1.0.zip' + if (-not ($Files = Get-ChildItem $DestinationPath -Filter "BCrypt.Net.dll" -Recurse)) { + if (-not (Test-Path $ZipPath)) { + Invoke-WebRequest -Uri 'https://www.nuget.org/api/v2/package/BCrypt.Net/0.1.0' -OutFile $ZipPath -UseBasicParsing + } + Expand-Archive -Path $ZipPath -DestinationPath $DestinationPath + $Files = Get-ChildItem $DestinationPath -Recurse + } + $Files.Where{$_.Name -eq 'BCrypt.Net.dll'}.FullName + } +} + +function Set-JenkinsPassword { + <# + .Synopsis + Sets the password for a Jenkins user. + .Example + Set-JenkinsPassword -UserName 'admin' -NewPassword $JenkinsCred.Password + # Sets the password to a known value + .Example + Set-JenkinsPassword -Credential $JenkinsCred + # Sets the password to a known value + .Example + $JenkinsCred = Set-JenkinsPassword -UserName 'admin' -NewPassword $(New-ServicePassword) -PassThru + # Sets the password and stores a credential object in $JenkinsCred. + .Notes + This probably will not work for federated and other non-standard accounts. + #> + [CmdletBinding(DefaultParameterSetName = 'Split')] + param( + # The credential of the user to try and set. + [Parameter(ParameterSetName = 'Cred', Mandatory, Position=0)] + [PSCredential]$Credential = [PSCredential]::new($UserName, $NewPassword), + + # The name of the user to forcibly set the password for. + [Parameter(ParameterSetName = 'Split', Mandatory, Position=0)] + [string]$UserName = $Credential.UserName, + + # The password to set for the user. + [Parameter(ParameterSetName = 'Split', Mandatory, Position=1)] + [SecureString]$NewPassword = $Credential.Password, + + # If set, passes the credential object for the user back. + [Parameter()] + [switch]$PassThru, + + # The path to the Jenkins data directory. + [Parameter()] + $JenkinsHome = (Join-Path $env:ProgramData "Jenkins\.jenkins") + ) + try { + $BCryptDllPath = Get-BcryptDll -ErrorAction Stop + Add-Type -Path $BCryptDllPath -ErrorAction Stop + } catch { + Write-Error "Could not get Bcrypt DLL:`n$_" + } + + $UserConfigPath = Resolve-Path "$JenkinsHome\users\$($UserName)_*\config.xml" + if ($UserConfigPath.Count -ne 1) { + Write-Error "$($UserConfigPath.Count) user config file(s) were found for user '$($UserName)'" + } + Write-Verbose "Updating '$($UserConfigPath)'" + + # Can't load as XML document as file is XML v1.1 + (Get-Content $UserConfigPath) -replace '#jbcrypt:.*', + "#jbcrypt:$( + [bcrypt.net.bcrypt]::hashpassword( + ([System.Net.NetworkCredential]$Credential).Password, + ([bcrypt.net.bcrypt]::generatesalt(15)) + ) + )" | + Set-Content $UserConfigPath -Force + + if ($PassThru) { + $Credential + } +} + +function Set-JenkinsLocationConfiguration { + <# + .Synopsis + Sets the jenkinsUrl in the location configuration file. + + .Example + Set-JenkinsURL -Url 'http://jenkins.fabrikam.com:8080' + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + # The full URI to access Jenkins on, including port and scheme. + [string]$Url, + + # The address to use as the admin e-mail address. + [string]$AdminAddress = 'address not configured yet <nobody@nowhere>', + + [string]$Path = "C:\ProgramData\Jenkins\.jenkins\jenkins.model.JenkinsLocationConfiguration.xml" + ) + @" + + +$AdminAddress +$Url + +"@ | Out-File -FilePath $Path -Encoding utf8 +} + +function Invoke-TextReplacementInFile { + [CmdletBinding()] + param( + # The path to the file(s) to replace text in. + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [Alias('FullName')] + [string]$Path, + + # The replacements to make, in a key-value format. + [hashtable]$Replacement + ) + process { + $Content = Get-Content -Path $Path -Raw + $Replacement.GetEnumerator().ForEach{ + $Content = $Content -replace $_.Key, $_.Value + } + $Content | Set-Content -Path $Path -NoNewline + } +} + +function Update-Clixml { + [CmdletBinding()] + param( + [Parameter()] + [string]$Path = "$env:SystemDrive\choco-setup\clixml\chocolatey-for-business.xml", + + [Parameter(Mandatory)] + [hashtable]$Properties + ) + $CliXml = if (Test-Path $Path) { + Import-Clixml $Path + } else { + if (-not (Test-Path (Split-Path $Path -Parent))) { + $null = mkdir (Split-Path $Path -Parent) -Force + } + [PSCustomObject]@{} + } + + $Properties.GetEnumerator().ForEach{ + Add-Member -InputObject $CliXml -MemberType NoteProperty -Name $_.Key -Value $_.Value -Force + } + + $CliXml | Export-Clixml $Path -Force +} + +function Get-ChocoEnvironmentProperty { + [CmdletBinding(DefaultParameterSetName="All")] + param( + [Parameter(ParameterSetName="Specific", Mandatory, ValueFromPipeline, Position=0)] + [string]$Name, + + [Parameter(ParameterSetName="Specific")] + [switch]$AsPlainText + ) + begin { + $Content = Import-Clixml -Path "$env:SystemDrive\choco-setup\clixml\chocolatey-for-business.xml" + } + process { + if ($Name) { + if ($AsPlainText -and $Content.$Name -is [System.Security.SecureString]) { + return $Content.$Name.ToPlainText() + } else { + return $Content.$Name + } + } else { + $Content + } + } +} + +function Set-ChocoEnvironmentProperty { + [CmdletBinding(DefaultParameterSetName="Key")] + param( + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName="Key", Position=0)] + [Alias('Key')] + [string]$Name, + + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName="Key", Position=1)] + $Value, + + [Parameter(Mandatory, ValueFromPipeline, ParameterSetName="Hashtable")] + [hashtable]$InputObject = @{} + ) + begin { + $Properties = $InputObject + } + process { + $Properties.$Name = $Value + } + end { + Update-Clixml -Path "$env:SystemDrive\choco-setup\clixml\chocolatey-for-business.xml" -Properties $Properties + } +} + +function Set-JenkinsCertificate { + <# + .Synopsis + Updates a keystore and ensure Jenkins is configured to use an appropriate port and certificate for HTTPS access + + .Example + Set-JenkinsCert -Thumbprint $Thumbprint + + .Notes + Requires a Jenkins service restart after the changes have been made. + #> + [CmdletBinding()] + param( + # The thumbprint of the certificate to use + [Parameter(Mandatory)] + [String]$Thumbprint, + + # The port to have HTTPS available on + [Parameter()] + [uint16]$Port = 7443 + ) + + $KeyStore = "C:\ProgramData\Jenkins\.jenkins\keystore.jks" + $KeyTool = Convert-Path "C:\Program Files\Eclipse Adoptium\jre-*.*\bin\keytool.exe" # Using Temurin jre package keytool + $Passkey = [System.Net.NetworkCredential]::new( + "JksPassword", + (New-ServicePassword -AvailableCharacters @(48..57 + 65..90 + 97..122)) + ).Password + + if (Test-Path $KeyStore) { + Remove-Item $KeyStore -Force + } + + # Generate the Keystore file + try { + $CertificatePath = Join-Path $env:Temp "$($Thumbprint).pfx" + $CertificatePassword = [System.Net.NetworkCredential]::new( + "TemporaryCertificatePassword", + (New-ServicePassword) + ) + + # Temporarily export the certificate as a PFX + $null = Get-ChildItem Cert:\LocalMachine\TrustedPeople\ | Where-Object {$_.Thumbprint -eq $Thumbprint} | Export-PfxCertificate -FilePath $CertificatePath -Password $CertificatePassword.SecurePassword + + # Using a job to hide improper non-output streams + $Job = Start-Job { + $CurrentAlias = ($($using:CertificatePassword.Password | & $using:KeyTool -list -v -storetype PKCS12 -keystore $using:CertificatePath -J"-Duser.language=en") -match "^Alias.*").Split(':')[1].Trim() + + $null = & $using:KeyTool -importkeystore -srckeystore $using:CertificatePath -srcstoretype PKCS12 -srcstorepass $using:CertificatePassword.Password -destkeystore $using:KeyStore -deststoretype JKS -alias $currentAlias -destalias jetty -deststorepass $using:Passkey + $null = & $using:KeyTool -keypasswd -keystore $using:KeyStore -alias jetty -storepass $using:Passkey -keypass $using:CertificatePassword.Password -new $using:Passkey + } | Wait-Job + if ($Job.State -eq 'Failed') { + $Job | Receive-Job + } else { + $Job | Remove-Job + } + } finally { + # Clean up the exported certificate + Remove-Item $CertificatePath + } + + # Update the Jenkins Configuration + $XmlPath = "C:\Program Files\Jenkins\jenkins.xml" + [xml]$Xml = Get-Content $XmlPath + @{ + httpPort = -1 + httpsPort = $Port + httpsKeyStore = $KeyStore + httpsKeyStorePassword = $Passkey + }.GetEnumerator().ForEach{ + if ($Xml.SelectSingleNode("/service/arguments")."#text" -notmatch [Regex]::Escape("--$($_.Key)=$($_.Value)")) { + $Xml.SelectSingleNode("/service/arguments")."#text" = $Xml.SelectSingleNode("/service/arguments")."#text" -replace "\s*--$($_.Key)=.+?\b", "" + $Xml.SelectSingleNode("/service/arguments")."#text" += " --$($_.Key)=$($_.Value)" + } + } + $Xml.Save($XmlPath) + + if ((Get-Service Jenkins).Status -eq 'Running') { + Restart-Service Jenkins + } +} +#endregion + +#region README functions +Function New-QuickstartReadme { + <# +.SYNOPSIS +Generates a desktop README file containing service information for all services provisioned as part of the Quickstart Guide. +.PARAMETER HostName +The host name of the C4B instance. +.EXAMPLE +./New-QuickstartReadme.ps1 +.EXAMPLE +./New-QuickstartReadme.ps1 -HostName c4b.example.com +#> + [CmdletBinding()] + param() + process { + try { + $Data = Get-ChocoEnvironmentProperty + } catch { + Write-Error "Unable to read stored values. Ensure the Quickstart Guide has been completed." + } + + Copy-Item $PSScriptRoot\ReadmeTemplate.html.j2 -Destination $env:Public\Desktop\Readme.html -Force + + # Working around the existing j2 template, so we can keep them roughly in sync + Invoke-TextReplacementInFile -Path $env:Public\Desktop\Readme.html -Replacement @{ + # CCM Values + "{{ ccm_fqdn .*?}}" = ([uri]$Data.CCMWebPortal).DnsSafeHost + "{{ ccm_port .*?}}" = ([uri]$Data.CCMWebPortal).Port + "{{ ccm_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.DefaultPwToBeChanged) + + # Chocolatey Configuration Values + "{{ ccm_encryption_password .*?}}" = "Requested on first run." + "{{ ccm_client_salt .*?}}" = [System.Web.HttpUtility]::HtmlEncode((Get-ChocoEnvironmentProperty ClientSalt -AsPlainText)) + "{{ ccm_service_salt .*?}}" = [System.Web.HttpUtility]::HtmlEncode((Get-ChocoEnvironmentProperty ServiceSalt -AsPlainText)) + "{{ chocouser_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.NexusCredential.Password.ToPlainText()) + + # Nexus Values + "{{ nexus_fqdn .*?}}" = ([uri]$Data.NexusUri).DnsSafeHost + "{{ nexus_port .*?}}" = ([uri]$Data.NexusUri).Port + "{{ nexus_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.NexusCredential.Password.ToPlainText()) + "{{ lookup\('file', 'credentials\/nexus_apikey'\) .*?}}" = Get-ChocoEnvironmentProperty NugetApiKey -AsPlainText + + # Jenkins Values + "{{ jenkins_fqdn .*?}}" = ([uri]$Data.JenkinsUri).DnsSafeHost + "{{ jenkins_port .*?}}" = ([uri]$Data.JenkinsUri).Port + "{{ jenkins_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.JenkinsCredential.Password.ToPlainText()) + } + } +} +#endregion + +#region Agent Setup +function Install-ChocolateyAgent { + [CmdletBinding()] + Param( + [Parameter()] + [String] + $Source, + + [Parameter(Mandatory)] + [String] + $CentralManagementServiceUrl, + + [Parameter()] + [String] + $ServiceSalt, + + [Parameter()] + [String] + $ClientSalt + ) + + process { + if ($Source) { + $chocoArgs = @('install', 'chocolatey-agent', '-y', "--source='$Source'") + & choco @chocoArgs + } + else { + $chocoArgs = @('install', 'chocolatey-agent', '-y') + & choco @chocoArgs + } + + + $chocoArgs = @('config', 'set', 'centralManagementServiceUrl', "$CentralManagementServiceUrl") + & choco @chocoArgs + + $chocoArgs = @('feature', 'enable', '--name="useChocolateyCentralManagement"') + & choco @chocoArgs + + $chocoArgs = @('feature', 'enable', '--name="useChocolateyCentralManagementDeployments"') + & choco @chocoArgs + + if ($ServiceSalt -and $ClientSalt) { + $chocoArgs = @('config', 'set', 'centralManagementClientCommunicationSaltAdditivePassword', "$ClientSalt") + & choco @chocoArgs + + $chocoArgs = @('config', 'set', 'centralManagementServiceCommunicationSaltAdditivePassword', "$ServiceSalt") + & choco @chocoArgs + } + } +} +#endregion + +Export-ModuleMember -Function "*" \ No newline at end of file diff --git a/scripts/ReadmeTemplate.html.j2 b/modules/C4B-Environment/ReadmeTemplate.html.j2 similarity index 100% rename from scripts/ReadmeTemplate.html.j2 rename to modules/C4B-Environment/ReadmeTemplate.html.j2 diff --git a/scripts/Get-Helpers.ps1 b/scripts/Get-Helpers.ps1 index 58d66cd..b8b1655 100644 --- a/scripts/Get-Helpers.ps1 +++ b/scripts/Get-Helpers.ps1 @@ -1,2321 +1 @@ -# Helper Functions for the various QSG scripts -function Invoke-Choco { - [CmdletBinding()] - [Alias('choco')] - param( - [Parameter(Position=0)] - [string]$Command, - - [Parameter(Position=1, ValueFromRemainingArguments)] - [string[]]$Arguments, - - [int[]]$ValidExitCodes = @(0) - ) - - if ($Command -eq 'Install' -and $Arguments -notmatch '\b-(y|-confirm)\b') { - $Arguments += '--confirm' - } - - if ($Arguments -notmatch '\b-(r|-limitoutput|-limit-output)\b') { - $Arguments += '--limit-output' - } - - $chocoPath = if ($CommandPath = Get-Command choco.exe -ErrorAction SilentlyContinue) { - $CommandPath.Source - } elseif ($env:ChocolateyInstall) { - Join-Path $env:ChocolateyInstall "choco.exe" - } elseif (Test-Path C:\ProgramData\chocolatey\choco.exe) { - "C:\ProgramData\chocolatey\choco.exe" - } else { - Write-Error "Could not find 'choco.exe' - unexpected behaviour is expected!" - "choco.exe" - } - - & $chocoPath $Command $Arguments | Tee-Object -Variable Result | Where-Object {$_} | ForEach-Object { - Write-Information -MessageData $_ -Tags Choco - } - - if ($LASTEXITCODE -notin $ValidExitCodes) { - Write-Error -Message "$($Result[-5..-1] -join "`n")" -TargetObject "choco $Command $Arguments" - } -} - -Update-TypeData -TypeName SecureString -MemberType ScriptMethod -MemberName ToPlainText -Force -Value { - [System.Net.NetworkCredential]::new("TempCredential", $this).Password -} - -#region Package functions (OfflineInstallPreparation.ps1) -if (-not ("System.IO.Compression.ZipArchive" -as [type])) { - Add-Type -Assembly 'System.IO.Compression' -} - -function Find-FileInArchive { - <# - .Synopsis - Finds files with a name matching a pattern in an archive. - .Example - Find-FileInArchive -Path "C:\Archive.zip" -like "tools/files/*-x86.exe" - .Example - Find-FileInArchive -Path $Nupkg -match "tools/files/dotnetcore-sdk-(?\d+\.\d+\.\d+)-win-x86\.exe(\.ignore)?" - .Notes - Please be aware that this matches against the full name of the file, not just the file name. - Though given that, you can easily write something to match the file name. - #> - [CmdletBinding(DefaultParameterSetName = "match")] - param( - # Path to the archive - [Parameter(Mandatory)] - [string]$Path, - - # Pattern to match with regex - [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = "match")] - [string]$match, - - # Pattern to match with basic globbing - [Parameter(Mandatory, ParameterSetName = "like")] - [string]$like - ) - begin { - while (-not $Zip -and $AccessRetries++ -lt 3) { - try { - $Stream = [IO.FileStream]::new($Path, [IO.FileMode]::Open) - $Zip = [IO.Compression.ZipArchive]::new($Stream, [IO.Compression.ZipArchiveMode]::Read) - } catch [System.IO.IOException] { - if ($AccessRetries -ge 3) { - Write-Error -Message "Accessing '$Path' failed after $AccessRetries attempts." -TargetObject $Path - } else { - Write-Information "Could not access '$Path', retrying..." - Start-Sleep -Milliseconds 500 - } - } - } - } - process { - if ($Zip) { - # Improve "security"? - $WhereBlock = [ScriptBlock]::Create("`$_.FullName -$($PSCmdlet.ParameterSetName) '$(Get-Variable -Name $PSCmdlet.ParameterSetName -ValueOnly)'") - $Zip.Entries | Where-Object -FilterScript $WhereBlock - } - } - end { - if ($Zip) { - $Zip.Dispose() - } - if ($Stream) { - $Stream.Close() - $Stream.Dispose() - } - } -} - -function Get-FileContentInArchive { - <# - .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(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 - } - } -} - -function New-NexusRole { - <# - .SYNOPSIS - Creates a new Nexus Role - - .DESCRIPTION - Creates a new Nexus Role - - .PARAMETER Id - The ID of the role - - .PARAMETER Name - The friendly name of the role - - .PARAMETER Description - A description of the role - - .PARAMETER Privileges - Included privileges for the role - - .PARAMETER Roles - Included nested roles - - .EXAMPLE - New-NexusRole -Id SamepleRole - - .EXAMPLE - New-NexusRole -Id SampleRole -Description "A sample role" -Privileges nx-all - - .NOTES - - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/NexuShell/Security/Roles/New-NexusRole/')] - Param( - [Parameter(Mandatory)] - [String] - $Id, - - [Parameter(Mandatory)] - [String] - $Name, - - [Parameter()] - [String] - $Description, - - [Parameter(Mandatory)] - [String[]] - $Privileges, - - [Parameter()] - [String[]] - $Roles - ) - - begin { - if (-not $header) { - throw 'Not connected to Nexus server! Run Connect-NexusServer first.' - } - } - - process { - - $urislug = '/service/rest/v1/security/roles' - $Body = @{ - - id = $Id - name = $Name - description = $Description - privileges = @($Privileges) - roles = $Roles - - } - - Invoke-Nexus -Urislug $urislug -Body $Body -Method POST | Foreach-Object { - [PSCustomobject]@{ - Id = $_.id - Name = $_.name - Description = $_.description - Privileges = $_.privileges - Roles = $_.roles - } - } - - } -} - -function Set-NexusAnonymousAuth { - <# - .SYNOPSIS - Turns Anonymous Authentication on or off in Nexus - - .DESCRIPTION - Turns Anonymous Authentication on or off in Nexus - - .PARAMETER Enabled - Turns on Anonymous Auth - - .PARAMETER Disabled - Turns off Anonymous Auth - - .EXAMPLE - Set-NexusAnonymousAuth -Enabled - #> - [CmdletBinding(HelpUri = 'https://steviecoaster.dev/NexuShell/Set-NexusAnonymousAuth/')] - Param( - [Parameter()] - [Switch] - $Enabled, - - [Parameter()] - [Switch] - $Disabled - ) - - begin { - - if (-not $header) { - throw "Not connected to Nexus server! Run Connect-NexusServer first." - } - - $urislug = "/service/rest/v1/security/anonymous" - } - - process { - - Switch ($true) { - - $Enabled { - $Body = @{ - enabled = $true - userId = 'anonymous' - realmName = 'NexusAuthorizingRealm' - } - - Invoke-Nexus -UriSlug $urislug -Body $Body -Method 'PUT' - } - - $Disabled { - $Body = @{ - enabled = $false - userId = 'anonymous' - realmName = 'NexusAuthorizingRealm' - } - - Invoke-Nexus -UriSlug $urislug -Body $Body -Method 'PUT' - - } - } - } -} - -#endregion - -#region SSL functions (Set-SslSecurity.ps1) - -function Get-Certificate { - [CmdletBinding()] - param( - [Parameter()] - [string] - $Thumbprint, - - [Parameter()] - [string] - $Subject - ) - - $filter = if ($Thumbprint) { - { $_.Thumbprint -eq $Thumbprint } - } - else { - { $_.Subject -like "CN=$Subject" } - } - - $cert = Get-ChildItem -Path Cert:\LocalMachine\My, Cert:\LocalMachine\TrustedPeople | - Where-Object $filter -ErrorAction Stop | - Select-Object -First 1 - - if ($null -eq $cert) { - throw "Certificate either not found, or other issue arose." - } - else { - Write-Host "Certification validation passed" -ForegroundColor Green - $cert - } -} - -function Copy-CertToStore { - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [System.Security.Cryptography.X509Certificates.X509Certificate2] - $Certificate - ) - - $location = [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine - $trustedCertStore = [System.Security.Cryptography.X509Certificates.X509Store]::new('TrustedPeople', $location) - - try { - $trustedCertStore.Open('ReadWrite') - $trustedCertStore.Add($Certificate) - } - finally { - $trustedCertStore.Close() - $trustedCertStore.Dispose() - } -} - -function Get-RemoteCertificate { - param( - [Parameter(Mandatory = $true, Position = 0)] - [string]$ComputerName, - - [Parameter(Position = 1)] - [UInt16]$Port = 8443 - ) - - $tcpClient = New-Object System.Net.Sockets.TcpClient($ComputerName, $Port) - $sslProtocolType = [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 - try { - $tlsClient = New-Object System.Net.Security.SslStream($tcpClient.GetStream(), 'false', $callback) - $tlsClient.AuthenticateAsClient($ComputerName, $null, $sslProtocolType, $false) - - return $tlsClient.RemoteCertificate -as [System.Security.Cryptography.X509Certificates.X509Certificate2] - } - finally { - if ($tlsClient -is [IDisposable]) { - $tlsClient.Dispose() - } - - $tcpClient.Dispose() - } -} - -function Set-NexusCert { - [CmdletBinding()] - param( - # The thumbprint of the certificate to configure Nexus to use, from the LocalMachine\TrustedPeople store. - [Parameter(Mandatory)] - $Thumbprint, - - # The port to set Nexus to use for https. - $Port = 8443 - ) - - $KeyTool = "C:\ProgramData\nexus\jre\bin\keytool.exe" - $KeyStorePath = 'C:\ProgramData\nexus\etc\ssl\keystore.jks' - $KeystoreCredential = [System.Net.NetworkCredential]::new( - "Keystore", - (New-ServicePassword) - ) - $TempCertPath = Join-Path $env:TEMP "$(New-Guid).pfx" - - try { - # Temporarily export the certificate as a PFX - Get-ChildItem Cert:\LocalMachine\TrustedPeople\ | Where-Object { $_.Thumbprint -eq $Thumbprint } | Sort-Object | Select-Object -First 1 | Export-PfxCertificate -FilePath $TempCertPath -Password $KeystoreCredential.SecurePassword - # TODO: Is this the right place for this? # Get-ChildItem -Path $TempCertPath | Import-PfxCertificate -CertStoreLocation Cert:\LocalMachine\My -Exportable -Password $KeystoreCredential.SecurePassword - - if (Test-Path $KeyStorePath) { - Remove-Item $KeyStorePath -Force - } - - # Using a job to hide improper non-output streams - $Job = Start-Job { - $string = ($using:KeystoreCredential.Password | & $using:KeyTool -list -v -keystore $using:TempCertPath -J"-Duser.language=en") -match '^Alias.*' - $currentAlias = ($string -split ':')[1].Trim() - & $using:KeyTool -importkeystore -srckeystore $using:TempCertPath -srcstoretype PKCS12 -srcstorepass $using:KeystoreCredential.Password -destkeystore $using:KeyStorePath -deststoretype JKS -alias $currentAlias -destalias jetty -deststorepass $using:KeystoreCredential.Password - & $using:KeyTool -keypasswd -keystore $using:KeyStorePath -alias jetty -storepass $using:KeystoreCredential.Password -keypass $using:KeystoreCredential.Password -new $using:KeystoreCredential.Password - } | Wait-Job - if ($Job.State -eq 'Failed') { - $Job | Receive-Job - } else { - $Job | Remove-Job - } - } finally { - if (Test-Path $TempCertPath) { - Remove-Item $TempCertPath -Force - } - } - - # Update the Nexus configuration - $xmlPath = 'C:\ProgramData\nexus\etc\jetty\jetty-https.xml' - [xml]$xml = Get-Content -Path 'C:\ProgramData\nexus\etc\jetty\jetty-https.xml' - foreach ($entry in $xml.Configure.New.Where{ $_.id -match 'ssl' }.Set.Where{ $_.name -match 'password' }) { - $entry.InnerText = $KeystoreCredential.Password - } - - $xml.Save($xmlPath) - - $configPath = "C:\ProgramData\sonatype-work\nexus3\etc\nexus.properties" - - # Remove existing ssl config from the configuration - (Get-Content $configPath) | Where-Object {$_ -notmatch "application-port-ssl="} | Set-Content $configPath - - # Ensure each line is added to the configuration - @( - 'jetty.https.stsMaxAge=-1' - "application-port-ssl=$Port" - 'nexus-args=${jetty.etc}/jetty.xml,${jetty.etc}/jetty-https.xml,${jetty.etc}/jetty-requestlog.xml' - ) | ForEach-Object { - if ((Get-Content -Raw $configPath) -notmatch [regex]::Escape($_)) { - $_ | Add-Content -Path $configPath - } - } - - if ((Get-Service Nexus).Status -eq 'Running') { - Restart-Service Nexus - } -} - -function Test-SelfSignedCertificate { - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline = $true)] - $Certificate = (Get-ChildItem -Path Cert:LocalMachine\My | Where-Object { $_.FriendlyName -eq $SubjectWithoutCn }) - ) - - process { - - if ($Certificate.Subject -eq $Certificate.Issuer) { - return $true - } - else { - return $false - } - - } - -} - -#endregion - -#region CCM functions (Start-C4bCcmSetup.ps1) -function Add-DatabaseUserAndRoles { - param( - [parameter(Mandatory = $true)][string] $Username, - [parameter(Mandatory = $true)][string] $DatabaseName, - [parameter(Mandatory = $false)][string] $DatabaseServer = 'localhost\SQLEXPRESS', - [parameter(Mandatory = $false)] $DatabaseRoles = @('db_datareader'), - [parameter(Mandatory = $false)][string] $DatabaseServerPermissionsOptions = 'Trusted_Connection=true;', - [parameter(Mandatory = $false)][switch] $CreateSqlUser, - [parameter(Mandatory = $false)][string] $SqlUserPw - ) - - $LoginOptions = "FROM WINDOWS WITH DEFAULT_DATABASE=[$DatabaseName]" - if ($CreateSqlUser) { - $LoginOptions = "WITH PASSWORD='$SqlUserPw', DEFAULT_DATABASE=[$DatabaseName], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF" - } - - $addUserSQLCommand = @" -USE [master] -IF EXISTS(SELECT * FROM msdb.sys.syslogins WHERE UPPER([name]) = UPPER('$Username')) -BEGIN -DROP LOGIN [$Username] -END - -CREATE LOGIN [$Username] $LoginOptions - -USE [$DatabaseName] -IF EXISTS(SELECT * FROM sys.sysusers WHERE UPPER([name]) = UPPER('$Username')) -BEGIN -DROP USER [$Username] -END - -CREATE USER [$Username] FOR LOGIN [$Username] - -"@ - - foreach ($DatabaseRole in $DatabaseRoles) { - $addUserSQLCommand += @" -ALTER ROLE [$DatabaseRole] ADD MEMBER [$Username] -"@ - } - - Write-Host "Adding $UserName to $DatabaseName with the following permissions: $($DatabaseRoles -Join ', ')" - Write-Debug "running the following: \n $addUserSQLCommand" - $Connection = New-Object System.Data.SQLClient.SQLConnection - $Connection.ConnectionString = "server='$DatabaseServer';database='master';$DatabaseServerPermissionsOptions" - $Connection.Open() - $Command = New-Object System.Data.SQLClient.SQLCommand - $Command.CommandText = $addUserSQLCommand - $Command.Connection = $Connection - $null = $Command.ExecuteNonQuery() - $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 - Get-Process chocolateysoftware.chocolateymanagement.web* | Stop-Process -ErrorAction SilentlyContinue -Force -} - -function Remove-CcmBinding { - [CmdletBinding()] - param() - - process { - Write-Verbose "Removing existing bindings" - netsh http delete sslcert ipport=0.0.0.0:443 - } -} - -function New-CcmBinding { - [CmdletBinding()] - param( - [string]$Thumbprint - ) - 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" - Get-WebBinding -Name ChocolateyCentralManagement | Remove-WebBinding - New-WebBinding -Name ChocolateyCentralManagement -Protocol https -Port 443 -SslFlags 0 -IpAddress '*' -} - -function Start-CcmService { - try { - Start-Service chocolatey-central-management -ErrorAction Stop - } - catch { - #Try again... - Start-Service chocolatey-central-management -ErrorAction SilentlyContinue - } - finally { - if ((Get-Service chocolatey-central-management).Status -ne 'Running') { - Write-Warning "Unable to start Chocolatey Central Management service, please start manually in Services.msc" - } - } - -} - -function Set-CcmCertificate { - [CmdletBinding()] - Param( - [Parameter(Mandatory)] - [String] - $CertificateThumbprint - ) - - process { - Stop-Service chocolatey-central-management - $jsonData = Get-Content $env:ChocolateyInstall\lib\chocolatey-management-service\tools\service\appsettings.json | ConvertFrom-Json - $jsonData.CertificateThumbprint = $CertificateThumbprint - $jsonData | ConvertTo-Json | Set-Content $env:chocolateyInstall\lib\chocolatey-management-service\tools\service\appsettings.json - Start-Service chocolatey-central-management - } -} - -#endregion - -#region Jenkins Setup - -# Function to generate Jenkins password -function New-ServicePassword { - <# - .Synopsis - Generates and returns a suitably secure password suited for support calls - #> - [CmdletBinding()] - [OutputType([System.Security.SecureString])] - param( - [ValidateRange(1,128)] - [int]$Length = 64, - - [char[]]$AvailableCharacters = @( - # Specifically excluding $, `, ;, #, etc such that pasting - # passwords into support scripts will be more predictable. - "!%()*+,-./<=>?@[\]^_" - 48..57 # 0-9 - 65..90 # A-Z - 97..122 # a-z - ).ForEach{[char[]]$_} - ) - end { - $NewPassword = [System.Security.SecureString]::new() - - while ($NewPassword.Length -lt $Length) { - $NewPassword.AppendChar(($AvailableCharacters | Get-Random)) - } - - $NewPassword - } -} - -function Get-BcryptDll { - <# - .Synopsis - Finds the Bcrypt DLL if present, or downloads it if missing. Returns the full path to the DLL. - .Example - $BCryptDllPath = Get-BcryptDll - .Example - $BCryptDllPath = Get-BcryptDll -DestinationPath ~\Downloads - #> - [CmdletBinding()] - [OutputType([string])] - param( - # The path to find the DLL within, or extract the DLL to if unfound. - [Parameter(Position = 0)] - [string]$DestinationPath = (Join-Path $PSScriptRoot "bcrypt.net.0.1.0") - ) - end { - if (-not (Test-Path $DestinationPath)) { - $null = New-Item -Path $DestinationPath -ItemType Directory -Force - } - $ZipPath = Join-Path $env:TEMP 'bcrypt.net.0.1.0.zip' - if (-not ($Files = Get-ChildItem $DestinationPath -Filter "BCrypt.Net.dll" -Recurse)) { - if (-not (Test-Path $ZipPath)) { - Invoke-WebRequest -Uri 'https://www.nuget.org/api/v2/package/BCrypt.Net/0.1.0' -OutFile $ZipPath -UseBasicParsing - } - Expand-Archive -Path $ZipPath -DestinationPath $DestinationPath - $Files = Get-ChildItem $DestinationPath -Recurse - } - $Files.Where{$_.Name -eq 'BCrypt.Net.dll'}.FullName - } -} - -function Set-JenkinsPassword { - <# - .Synopsis - Sets the password for a Jenkins user. - .Example - Set-JenkinsPassword -UserName 'admin' -NewPassword $JenkinsCred.Password - # Sets the password to a known value - .Example - Set-JenkinsPassword -Credential $JenkinsCred - # Sets the password to a known value - .Example - $JenkinsCred = Set-JenkinsPassword -UserName 'admin' -NewPassword $(New-ServicePassword) -PassThru - # Sets the password and stores a credential object in $JenkinsCred. - .Notes - This probably will not work for federated and other non-standard accounts. - #> - [CmdletBinding(DefaultParameterSetName = 'Split')] - param( - # The credential of the user to try and set. - [Parameter(ParameterSetName = 'Cred', Mandatory, Position=0)] - [PSCredential]$Credential = [PSCredential]::new($UserName, $NewPassword), - - # The name of the user to forcibly set the password for. - [Parameter(ParameterSetName = 'Split', Mandatory, Position=0)] - [string]$UserName = $Credential.UserName, - - # The password to set for the user. - [Parameter(ParameterSetName = 'Split', Mandatory, Position=1)] - [SecureString]$NewPassword = $Credential.Password, - - # If set, passes the credential object for the user back. - [Parameter()] - [switch]$PassThru, - - # The path to the Jenkins data directory. - [Parameter()] - $JenkinsHome = (Join-Path $env:ProgramData "Jenkins\.jenkins") - ) - try { - $BCryptDllPath = Get-BcryptDll -ErrorAction Stop - Add-Type -Path $BCryptDllPath -ErrorAction Stop - } catch { - Write-Error "Could not get Bcrypt DLL:`n$_" - } - - $UserConfigPath = Resolve-Path "$JenkinsHome\users\$($UserName)_*\config.xml" - if ($UserConfigPath.Count -ne 1) { - Write-Error "$($UserConfigPath.Count) user config file(s) were found for user '$($UserName)'" - } - Write-Verbose "Updating '$($UserConfigPath)'" - - # Can't load as XML document as file is XML v1.1 - (Get-Content $UserConfigPath) -replace '#jbcrypt:.*', - "#jbcrypt:$( - [bcrypt.net.bcrypt]::hashpassword( - ([System.Net.NetworkCredential]$Credential).Password, - ([bcrypt.net.bcrypt]::generatesalt(15)) - ) - )" | - Set-Content $UserConfigPath -Force - - if ($PassThru) { - $Credential - } -} - -function Set-JenkinsLocationConfiguration { - <# - .Synopsis - Sets the jenkinsUrl in the location configuration file. - - .Example - Set-JenkinsURL -Url 'http://jenkins.fabrikam.com:8080' - #> - [CmdletBinding()] - param( - [Parameter(Mandatory)] - # The full URI to access Jenkins on, including port and scheme. - [string]$Url, - - # The address to use as the admin e-mail address. - [string]$AdminAddress = 'address not configured yet <nobody@nowhere>', - - [string]$Path = "C:\ProgramData\Jenkins\.jenkins\jenkins.model.JenkinsLocationConfiguration.xml" - ) - @" - - -$AdminAddress -$Url - -"@ | Out-File -FilePath $Path -Encoding utf8 -} - -function Invoke-TextReplacementInFile { - [CmdletBinding()] - param( - # The path to the file(s) to replace text in. - [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] - [Alias('FullName')] - [string]$Path, - - # The replacements to make, in a key-value format. - [hashtable]$Replacement - ) - process { - $Content = Get-Content -Path $Path -Raw - $Replacement.GetEnumerator().ForEach{ - $Content = $Content -replace $_.Key, $_.Value - } - $Content | Set-Content -Path $Path -NoNewline - } -} - -function Update-Clixml { - [CmdletBinding()] - param( - [Parameter()] - [string]$Path = "$env:SystemDrive\choco-setup\clixml\chocolatey-for-business.xml", - - [Parameter(Mandatory)] - [hashtable]$Properties - ) - $CliXml = if (Test-Path $Path) { - Import-Clixml $Path - } else { - if (-not (Test-Path (Split-Path $Path -Parent))) { - $null = mkdir (Split-Path $Path -Parent) -Force - } - [PSCustomObject]@{} - } - - $Properties.GetEnumerator().ForEach{ - Add-Member -InputObject $CliXml -MemberType NoteProperty -Name $_.Key -Value $_.Value -Force - } - - $CliXml | Export-Clixml $Path -Force -} - -function Get-ChocoEnvironmentProperty { - [CmdletBinding(DefaultParameterSetName="All")] - param( - [Parameter(ParameterSetName="Specific", Mandatory, ValueFromPipeline, Position=0)] - [string]$Name, - - [Parameter(ParameterSetName="Specific")] - [switch]$AsPlainText - ) - begin { - $Content = Import-Clixml -Path "$env:SystemDrive\choco-setup\clixml\chocolatey-for-business.xml" - } - process { - if ($Name) { - if ($AsPlainText -and $Content.$Name -is [System.Security.SecureString]) { - return $Content.$Name.ToPlainText() - } else { - return $Content.$Name - } - } else { - $Content - } - } -} - -function Set-ChocoEnvironmentProperty { - [CmdletBinding(DefaultParameterSetName="Key")] - param( - [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName="Key", Position=0)] - [Alias('Key')] - [string]$Name, - - [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName="Key", Position=1)] - $Value, - - [Parameter(Mandatory, ValueFromPipeline, ParameterSetName="Hashtable")] - [hashtable]$InputObject = @{} - ) - begin { - $Properties = $InputObject - } - process { - $Properties.$Name = $Value - } - end { - Update-Clixml -Path "$env:SystemDrive\choco-setup\clixml\chocolatey-for-business.xml" -Properties $Properties - } -} - -function Set-JenkinsCertificate { - <# - .Synopsis - Updates a keystore and ensure Jenkins is configured to use an appropriate port and certificate for HTTPS access - - .Example - Set-JenkinsCert -Thumbprint $Thumbprint - - .Notes - Requires a Jenkins service restart after the changes have been made. - #> - [CmdletBinding()] - param( - # The thumbprint of the certificate to use - [Parameter(Mandatory)] - [String]$Thumbprint, - - # The port to have HTTPS available on - [Parameter()] - [uint16]$Port = 7443 - ) - - $KeyStore = "C:\ProgramData\Jenkins\.jenkins\keystore.jks" - $KeyTool = Convert-Path "C:\Program Files\Eclipse Adoptium\jre-*.*\bin\keytool.exe" # Using Temurin jre package keytool - $Passkey = [System.Net.NetworkCredential]::new( - "JksPassword", - (New-ServicePassword -AvailableCharacters @(48..57 + 65..90 + 97..122)) - ).Password - - if (Test-Path $KeyStore) { - Remove-Item $KeyStore -Force - } - - # Generate the Keystore file - try { - $CertificatePath = Join-Path $env:Temp "$($Thumbprint).pfx" - $CertificatePassword = [System.Net.NetworkCredential]::new( - "TemporaryCertificatePassword", - (New-ServicePassword) - ) - - # Temporarily export the certificate as a PFX - $null = Get-ChildItem Cert:\LocalMachine\TrustedPeople\ | Where-Object {$_.Thumbprint -eq $Thumbprint} | Export-PfxCertificate -FilePath $CertificatePath -Password $CertificatePassword.SecurePassword - - # Using a job to hide improper non-output streams - $Job = Start-Job { - $CurrentAlias = ($($using:CertificatePassword.Password | & $using:KeyTool -list -v -storetype PKCS12 -keystore $using:CertificatePath -J"-Duser.language=en") -match "^Alias.*").Split(':')[1].Trim() - - $null = & $using:KeyTool -importkeystore -srckeystore $using:CertificatePath -srcstoretype PKCS12 -srcstorepass $using:CertificatePassword.Password -destkeystore $using:KeyStore -deststoretype JKS -alias $currentAlias -destalias jetty -deststorepass $using:Passkey - $null = & $using:KeyTool -keypasswd -keystore $using:KeyStore -alias jetty -storepass $using:Passkey -keypass $using:CertificatePassword.Password -new $using:Passkey - } | Wait-Job - if ($Job.State -eq 'Failed') { - $Job | Receive-Job - } else { - $Job | Remove-Job - } - } finally { - # Clean up the exported certificate - Remove-Item $CertificatePath - } - - # Update the Jenkins Configuration - $XmlPath = "C:\Program Files\Jenkins\jenkins.xml" - [xml]$Xml = Get-Content $XmlPath - @{ - httpPort = -1 - httpsPort = $Port - httpsKeyStore = $KeyStore - httpsKeyStorePassword = $Passkey - }.GetEnumerator().ForEach{ - if ($Xml.SelectSingleNode("/service/arguments")."#text" -notmatch [Regex]::Escape("--$($_.Key)=$($_.Value)")) { - $Xml.SelectSingleNode("/service/arguments")."#text" = $Xml.SelectSingleNode("/service/arguments")."#text" -replace "\s*--$($_.Key)=.+?\b", "" - $Xml.SelectSingleNode("/service/arguments")."#text" += " --$($_.Key)=$($_.Value)" - } - } - $Xml.Save($XmlPath) - - if ((Get-Service Jenkins).Status -eq 'Running') { - Restart-Service Jenkins - } -} -#endregion - -#region README functions -Function New-QuickstartReadme { - <# -.SYNOPSIS -Generates a desktop README file containing service information for all services provisioned as part of the Quickstart Guide. -.PARAMETER HostName -The host name of the C4B instance. -.EXAMPLE -./New-QuickstartReadme.ps1 -.EXAMPLE -./New-QuickstartReadme.ps1 -HostName c4b.example.com -#> - [CmdletBinding()] - param() - process { - try { - $Data = Get-ChocoEnvironmentProperty - } catch { - Write-Error "Unable to read stored values. Ensure the Quickstart Guide has been completed." - } - - Copy-Item $PSScriptRoot\ReadmeTemplate.html.j2 -Destination $env:Public\Desktop\Readme.html -Force - - # Working around the existing j2 template, so we can keep them roughly in sync - Invoke-TextReplacementInFile -Path $env:Public\Desktop\Readme.html -Replacement @{ - # CCM Values - "{{ ccm_fqdn .*?}}" = ([uri]$Data.CCMWebPortal).DnsSafeHost - "{{ ccm_port .*?}}" = ([uri]$Data.CCMWebPortal).Port - "{{ ccm_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.DefaultPwToBeChanged) - - # Chocolatey Configuration Values - "{{ ccm_encryption_password .*?}}" = "Requested on first run." - "{{ ccm_client_salt .*?}}" = [System.Web.HttpUtility]::HtmlEncode((Get-ChocoEnvironmentProperty ClientSalt -AsPlainText)) - "{{ ccm_service_salt .*?}}" = [System.Web.HttpUtility]::HtmlEncode((Get-ChocoEnvironmentProperty ServiceSalt -AsPlainText)) - "{{ chocouser_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.NexusCredential.Password.ToPlainText()) - - # Nexus Values - "{{ nexus_fqdn .*?}}" = ([uri]$Data.NexusUri).DnsSafeHost - "{{ nexus_port .*?}}" = ([uri]$Data.NexusUri).Port - "{{ nexus_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.NexusCredential.Password.ToPlainText()) - "{{ lookup\('file', 'credentials\/nexus_apikey'\) .*?}}" = Get-ChocoEnvironmentProperty NugetApiKey -AsPlainText - - # Jenkins Values - "{{ jenkins_fqdn .*?}}" = ([uri]$Data.JenkinsUri).DnsSafeHost - "{{ jenkins_port .*?}}" = ([uri]$Data.JenkinsUri).Port - "{{ jenkins_password .*?}}" = [System.Web.HttpUtility]::HtmlEncode($Data.JenkinsCredential.Password.ToPlainText()) - } - } -} -#endregion - -#region Agent Setup -function Install-ChocolateyAgent { - [CmdletBinding()] - Param( - [Parameter()] - [String] - $Source, - - [Parameter(Mandatory)] - [String] - $CentralManagementServiceUrl, - - [Parameter()] - [String] - $ServiceSalt, - - [Parameter()] - [String] - $ClientSalt - ) - - process { - if ($Source) { - $chocoArgs = @('install', 'chocolatey-agent', '-y', "--source='$Source'") - & choco @chocoArgs - } - else { - $chocoArgs = @('install', 'chocolatey-agent', '-y') - & choco @chocoArgs - } - - - $chocoArgs = @('config', 'set', 'centralManagementServiceUrl', "$CentralManagementServiceUrl") - & choco @chocoArgs - - $chocoArgs = @('feature', 'enable', '--name="useChocolateyCentralManagement"') - & choco @chocoArgs - - $chocoArgs = @('feature', 'enable', '--name="useChocolateyCentralManagementDeployments"') - & choco @chocoArgs - - if ($ServiceSalt -and $ClientSalt) { - $chocoArgs = @('config', 'set', 'centralManagementClientCommunicationSaltAdditivePassword', "$ClientSalt") - & choco @chocoArgs - - $chocoArgs = @('config', 'set', 'centralManagementServiceCommunicationSaltAdditivePassword', "$ServiceSalt") - & choco @chocoArgs - } - } -} -#endregion \ No newline at end of file +Import-Module $PSScriptRoot\..\modules\c4b-environment \ No newline at end of file diff --git a/tests/nexus.tests.ps1 b/tests/nexus.tests.ps1 index 1559c8b..df30c15 100644 --- a/tests/nexus.tests.ps1 +++ b/tests/nexus.tests.ps1 @@ -84,7 +84,7 @@ Describe "Nexus Configuration" { Context "Package Availability" { BeforeAll { if (([version] (C:\ProgramData\chocolatey\choco.exe --version).Split('-')[0]) -ge [version] '2.1.0') { - choco cache remove + C:\ProgramData\chocolatey\choco.exe cache remove } $packages = C:\ProgramData\chocolatey\choco.exe search -s ChocolateyInternal -r | ConvertFrom-Csv -Delimiter '|' -Header Package, Version