diff --git a/src/SdnDiagnostics.psm1 b/src/SdnDiagnostics.psm1 index e2ea80ae..d02820f6 100644 --- a/src/SdnDiagnostics.psm1 +++ b/src/SdnDiagnostics.psm1 @@ -38,6 +38,14 @@ New-Variable -Name 'SdnDiagnostics' -Scope 'Global' -Force -Value @{ # defines the current role(s) determined for the current node # supported values are 'Common', 'Gateway', 'NetworkController', 'Server', 'LoadBalancerMux' Role = @() + + # when set to $true, PSRemoting sessions will use WinRM over HTTPS (port 5986 by default) + # configure this when CIS hardening or other security policies disable WinRM over HTTP + UseSSL = $false + + # overrides the default WinRM port used when creating PSRemoting sessions + # if not set, defaults to 5985 (HTTP) or 5986 (HTTPS) based on UseSSL + Port = 0 } } @@ -751,7 +759,15 @@ function Start-SdnDataCollection { [Parameter(Mandatory = $false, ParameterSetName = 'Role')] [Parameter(Mandatory = $false, ParameterSetName = 'Computer')] - [bool]$ConvertETW = $true + [bool]$ConvertETW = $true, + + [Parameter(Mandatory = $false, ParameterSetName = 'Role')] + [Parameter(Mandatory = $false, ParameterSetName = 'Computer')] + [Switch]$UseSSL, + + [Parameter(Mandatory = $false, ParameterSetName = 'Role')] + [Parameter(Mandatory = $false, ParameterSetName = 'Computer')] + [System.Int32]$Port ) # if we are running in a remote session, we need to do some extra validation @@ -765,6 +781,16 @@ function Start-SdnDataCollection { } } + # if UseSSL or Port were explicitly provided, update the global config so that all downstream + # PSRemoting session creation functions pick up the correct transport settings + if ($PSBoundParameters.ContainsKey('UseSSL')) { + $Global:SdnDiagnostics.Config.UseSSL = $UseSSL.IsPresent + } + + if ($PSBoundParameters.ContainsKey('Port')) { + $Global:SdnDiagnostics.Config.Port = $Port + } + $ErrorActionPreference = 'Continue' $dataCollectionNodes = [System.Collections.ArrayList]::new() # need an arrayList so we can remove objects from this list @@ -927,18 +953,31 @@ function Start-SdnDataCollection { $Global:ProgressPreference = 'SilentlyContinue' $nodesToRemove = [System.Collections.ArrayList]::new() - $tncScriptBlock = { - $tncResult = Test-NetConnection -ComputerName $_.Name -Port 5985 -InformationLevel Quiet - if (-NOT ($tncResult)) { - [void]$nodesToRemove.Add($_) - } + if ($Global:SdnDiagnostics.Config.Port -gt 0) { + $tncPort = $Global:SdnDiagnostics.Config.Port + } + elseif ($Global:SdnDiagnostics.Config.UseSSL) { + $tncPort = 5986 + } + else { + $tncPort = 5985 } if ($PSVersionTable.PSVersion.Major -ge 7) { - $dataCollectionNodes | Foreach-Object -ThrottleLimit 10 -Parallel $tncScriptBlock + $dataCollectionNodes | Foreach-Object -ThrottleLimit 10 -Parallel { + $tncResult = Test-NetConnection -ComputerName $_.Name -Port $using:tncPort -InformationLevel Quiet + if (-NOT ($tncResult)) { + [void]($using:nodesToRemove).Add($_) + } + } } else { - $dataCollectionNodes | ForEach-Object $tncScriptBlock + $dataCollectionNodes | ForEach-Object { + $tncResult = Test-NetConnection -ComputerName $_.Name -Port $tncPort -InformationLevel Quiet + if (-NOT ($tncResult)) { + [void]$nodesToRemove.Add($_) + } + } } if ($nodesToRemove.Count -gt 0) { diff --git a/src/modules/SdnDiag.Utilities.psm1 b/src/modules/SdnDiag.Utilities.psm1 index ccd93c17..5c6e8e3e 100644 --- a/src/modules/SdnDiag.Utilities.psm1 +++ b/src/modules/SdnDiag.Utilities.psm1 @@ -495,7 +495,13 @@ function Copy-FileFromRemoteComputer { [Switch]$Recurse, [Parameter(Mandatory = $false)] - [Switch]$Force + [Switch]$Force, + + [Parameter(Mandatory = $false)] + [Switch]$UseSSL, + + [Parameter(Mandatory = $false)] + [System.Int32]$Port ) try { @@ -520,8 +526,25 @@ function Copy-FileFromRemoteComputer { catch { "{0}. Attempting to copy files using WinRM" -f $_ | Trace-Output -Level:Warning + $winRmParams = @{ + Path = $Path + ComputerName = $object + Destination = $Destination + Force = $Force.IsPresent + Recurse = $Recurse.IsPresent + Credential = $Credential + } + + if ($PSBoundParameters.ContainsKey('UseSSL')) { + $winRmParams.Add('UseSSL', $UseSSL) + } + + if ($PSBoundParameters.ContainsKey('Port')) { + $winRmParams.Add('Port', $Port) + } + try { - Copy-FileFromRemoteComputerWinRM -Path $Path -ComputerName $object -Destination $Destination -Force:($Force.IsPresent) -Recurse:($Recurse.IsPresent) -Credential $Credential + Copy-FileFromRemoteComputerWinRM @winRmParams } catch { # Catch the copy failed exception to not stop the copy for other computers which might success @@ -673,10 +696,29 @@ function Copy-FileFromRemoteComputerWinRM { [Switch]$Recurse, [Parameter(Mandatory = $false)] - [Switch]$Force + [Switch]$Force, + + [Parameter(Mandatory = $false)] + [Switch]$UseSSL, + + [Parameter(Mandatory = $false)] + [System.Int32]$Port ) - $session = New-PSRemotingSession -ComputerName $ComputerName -Credential $Credential + $sessionParams = @{ + ComputerName = $ComputerName + Credential = $Credential + } + + if ($PSBoundParameters.ContainsKey('UseSSL')) { + $sessionParams.Add('UseSSL', $UseSSL) + } + + if ($PSBoundParameters.ContainsKey('Port')) { + $sessionParams.Add('Port', $Port) + } + + $session = New-PSRemotingSession @sessionParams if ($session) { foreach ($subPath in $Path) { "Copying {0} to {1} using WinRM Session {2}" -f $subPath, $Destination.FullName, $session.Name | Trace-Output @@ -729,7 +771,13 @@ function Copy-FileToRemoteComputer { [Switch]$Recurse, [Parameter(Mandatory = $false)] - [Switch]$Force + [Switch]$Force, + + [Parameter(Mandatory = $false)] + [Switch]$UseSSL, + + [Parameter(Mandatory = $false)] + [System.Int32]$Port ) try { @@ -754,8 +802,25 @@ function Copy-FileToRemoteComputer { catch { "{0}. Attempting to copy files using WinRM" -f $_ | Trace-Output -Level:Warning + $winRmParams = @{ + Path = $Path + ComputerName = $object + Destination = $Destination + Credential = $Credential + Force = $Force.IsPresent + Recurse = $Recurse.IsPresent + } + + if ($PSBoundParameters.ContainsKey('UseSSL')) { + $winRmParams.Add('UseSSL', $UseSSL) + } + + if ($PSBoundParameters.ContainsKey('Port')) { + $winRmParams.Add('Port', $Port) + } + try { - Copy-FileToRemoteComputerWinRM -Path $Path -ComputerName $object -Destination $Destination -Credential $Credential -Force:($Force.IsPresent) -Recurse:($Recurse.IsPresent) + Copy-FileToRemoteComputerWinRM @winRmParams } catch { # Catch the copy failed exception to not stop the copy for other computers which might success @@ -904,10 +969,29 @@ function Copy-FileToRemoteComputerWinRM { [Switch]$Recurse, [Parameter(Mandatory = $false)] - [Switch]$Force + [Switch]$Force, + + [Parameter(Mandatory = $false)] + [Switch]$UseSSL, + + [Parameter(Mandatory = $false)] + [System.Int32]$Port ) - $session = New-PSRemotingSession -ComputerName $ComputerName -Credential $Credential + $sessionParams = @{ + ComputerName = $ComputerName + Credential = $Credential + } + + if ($PSBoundParameters.ContainsKey('UseSSL')) { + $sessionParams.Add('UseSSL', $UseSSL) + } + + if ($PSBoundParameters.ContainsKey('Port')) { + $sessionParams.Add('Port', $Port) + } + + $session = New-PSRemotingSession @sessionParams if ($session) { # copy the files to the destination using WinRM foreach ($subPath in $Path) { @@ -1558,7 +1642,13 @@ function Invoke-PSRemoteCommand { [System.String]$Activity, [Parameter(Mandatory = $false, ParameterSetName = 'AsJob')] - [int]$ExecutionTimeout = 900 + [int]$ExecutionTimeout = 900, + + [Parameter(Mandatory = $false)] + [Switch]$UseSSL, + + [Parameter(Mandatory = $false)] + [System.Int32]$Port ) $params = @{ @@ -1574,6 +1664,14 @@ function Invoke-PSRemoteCommand { $psSessionParams.Add('ImportModuleOnRemoteSession', $ImportModuleOnRemoteSession) } + if ($PSBoundParameters.ContainsKey('UseSSL')) { + $psSessionParams.Add('UseSSL', $UseSSL) + } + + if ($PSBoundParameters.ContainsKey('Port')) { + $psSessionParams.Add('Port', $Port) + } + $session = New-PSRemotingSession @psSessionParams if ($session) { $params.Add('Session', $session) @@ -1847,13 +1945,34 @@ function New-PSRemotingSession { [System.String]$ModuleName = $Global:SdnDiagnostics.Config.ModuleName, [Parameter(Mandatory = $false)] - [Switch]$Force + [Switch]$Force, + + [Parameter(Mandatory = $false)] + [Switch]$UseSSL, + + [Parameter(Mandatory = $false)] + [System.Int32]$Port ) begin { [bool]$disableSeeding = $Global:SdnDiagnostics.Config.DisableModuleSeeding [bool]$importModuleOnRemoteSession = $Global:SdnDiagnostics.Config.ImportModuleOnRemoteSession + # if UseSSL was not explicitly provided, fall back to global config + if (-NOT $PSBoundParameters.ContainsKey('UseSSL')) { + $UseSSL = [bool]$Global:SdnDiagnostics.Config.UseSSL + } + + # set the default port based on whether SSL is being used, then check global config override + if (-NOT $PSBoundParameters.ContainsKey('Port')) { + if ($Global:SdnDiagnostics.Config.Port -gt 0) { + $Port = $Global:SdnDiagnostics.Config.Port + } + else { + $Port = if ($UseSSL) { 5986 } else { 5985 } + } + } + $importRemoteModule = { param([string]$arg0, $arg1, $arg2) try { @@ -1910,8 +2029,9 @@ function New-PSRemotingSession { # determine if an IP address was passed for the destination # if using IP address it needs to be added to the trusted hosts + # when UseSSL is enabled, TrustedHosts management is not required as the server identity is validated via certificate $isIpAddress = ($objectName -as [IPAddress]) -as [Bool] - if ($isIpAddress) { + if ($isIpAddress -and -NOT $UseSSL) { try { Confirm-IsAdmin @@ -1929,9 +2049,38 @@ function New-PSRemotingSession { } try { + $newPSSessionParams = @{ + Name = "SdnDiag-$(Get-Random)" + ComputerName = $objectName + Port = $Port + ErrorAction = 'Stop' + } + + # Build session options only with parameters supported by the current platform + # WinRM-specific parameters (Culture, UICulture, IdleTimeout) are only available on Windows + $psSessionOptionCmd = Get-Command New-PSSessionOption -ErrorAction SilentlyContinue + if ($psSessionOptionCmd) { + $sessionOptionParams = @{} + if ($psSessionOptionCmd.Parameters.ContainsKey('IdleTimeout')) { + $sessionOptionParams['IdleTimeout'] = 86400000 + } + if ($psSessionOptionCmd.Parameters.ContainsKey('Culture')) { + $sessionOptionParams['Culture'] = 'en-US' + $sessionOptionParams['UICulture'] = 'en-US' + } + if ($sessionOptionParams.Count -gt 0) { + $newPSSessionParams.Add('SessionOption', (New-PSSessionOption @sessionOptionParams)) + } + } + + if ($UseSSL) { + $newPSSessionParams.Add('UseSSL', $true) + "PSRemotingSession will use SSL on port {0}" -f $Port | Trace-Output -Level:Verbose + } + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { "PSRemotingSession use user-defined credential" | Trace-Output -Level:Verbose - $session = New-PSSession -Name "SdnDiag-$(Get-Random)" -ComputerName $objectName -Credential $Credential -SessionOption (New-PSSessionOption -Culture en-US -UICulture en-US -IdleTimeout 86400000) -ErrorAction Stop + $newPSSessionParams.Add('Credential', $Credential) } else { # if the credential is not defined, we want to check if we @@ -1941,14 +2090,15 @@ function New-PSRemotingSession { # if we need to create a new remote session, need to check to ensure that if using an IP Address that credentials are specified # which is a requirement from a WinRM perspective. Will throw a warning and skip session creation for this computer. - if ($isIpAddress -and $Credential -eq [System.Management.Automation.PSCredential]::Empty) { + if ($isIpAddress -and -NOT $UseSSL -and $Credential -eq [System.Management.Automation.PSCredential]::Empty) { throw New-Object System.NotSupportedException("Unable to create PSSession to $objectName. The Credential parameter is required when using an IP Address.") } "PSRemotingSession use default credential" | Trace-Output -Level:Verbose - $session = New-PSSession -Name "SdnDiag-$(Get-Random)" -ComputerName $objectName -SessionOption (New-PSSessionOption -Culture 'en-US' -UICulture 'en-US' -IdleTimeout 86400000) -ErrorAction Stop } + $session = New-PSSession @newPSSessionParams + "Created powershell session {0} to {1}" -f $session.Name, $objectName | Trace-Output -Level:Verbose if ($ImportModuleOnRemoteSession) { "Importing module {0} on remote session {1}" -f $ModuleName, $session.Name | Trace-Output -Level:Verbose diff --git a/tests/offline/Utilities.Tests.ps1 b/tests/offline/Utilities.Tests.ps1 index a00aef64..6ea1d8f0 100644 --- a/tests/offline/Utilities.Tests.ps1 +++ b/tests/offline/Utilities.Tests.ps1 @@ -177,3 +177,136 @@ Describe 'Utilities - IP Address Validation' { } } } + + +Describe 'New-PSRemotingSession - WinRM over HTTPS' -Tag 'Unit' { + Context 'Port defaults' { + It "Uses port 5985 by default (HTTP)" { + InModuleScope SdnDiag.Utilities { + Mock Trace-Output {} + Mock Get-PSSession { return @() } + Mock New-PSSession { + return [PSCustomObject]@{ + Name = 'SdnDiag-Test' + ComputerName = $ComputerName + State = 'Opened' + Availability = 'Available' + Id = 1 + } + } + + New-PSRemotingSession -ComputerName 'DVLAB-S1-N01' + + Should -Invoke New-PSSession -Times 1 -ParameterFilter { + $Port -eq 5985 -and (-not $UseSSL) + } + } + } + + It "Uses port 5986 when -UseSSL is specified" { + InModuleScope SdnDiag.Utilities { + Mock Trace-Output {} + Mock Get-PSSession { return @() } + Mock New-PSSession { + return [PSCustomObject]@{ + Name = 'SdnDiag-Test' + ComputerName = $ComputerName + State = 'Opened' + Availability = 'Available' + Id = 1 + } + } + + New-PSRemotingSession -ComputerName 'DVLAB-S1-N01' -UseSSL + + Should -Invoke New-PSSession -Times 1 -ParameterFilter { + $Port -eq 5986 -and $UseSSL -eq $true + } + } + } + + It "Uses custom port when -Port is specified with -UseSSL" { + InModuleScope SdnDiag.Utilities { + Mock Trace-Output {} + Mock Get-PSSession { return @() } + Mock New-PSSession { + return [PSCustomObject]@{ + Name = 'SdnDiag-Test' + ComputerName = $ComputerName + State = 'Opened' + Availability = 'Available' + Id = 1 + } + } + + New-PSRemotingSession -ComputerName 'DVLAB-S1-N01' -UseSSL -Port 5988 + + Should -Invoke New-PSSession -Times 1 -ParameterFilter { + $Port -eq 5988 -and $UseSSL -eq $true + } + } + } + } + + Context 'Global config UseSSL and Port' { + It "Reads UseSSL from global config when not explicitly passed" { + InModuleScope SdnDiag.Utilities { + Mock Trace-Output {} + Mock Get-PSSession { return @() } + Mock New-PSSession { + return [PSCustomObject]@{ + Name = 'SdnDiag-Test' + ComputerName = $ComputerName + State = 'Opened' + Availability = 'Available' + Id = 1 + } + } + + $Global:SdnDiagnostics.Config.UseSSL = $true + $Global:SdnDiagnostics.Config.Port = 0 + try { + New-PSRemotingSession -ComputerName 'DVLAB-S1-N01' + + Should -Invoke New-PSSession -Times 1 -ParameterFilter { + $Port -eq 5986 -and $UseSSL -eq $true + } + } + finally { + $Global:SdnDiagnostics.Config.UseSSL = $false + $Global:SdnDiagnostics.Config.Port = 0 + } + } + } + + It "Reads Port from global config when not explicitly passed" { + InModuleScope SdnDiag.Utilities { + Mock Trace-Output {} + Mock Get-PSSession { return @() } + Mock New-PSSession { + return [PSCustomObject]@{ + Name = 'SdnDiag-Test' + ComputerName = $ComputerName + State = 'Opened' + Availability = 'Available' + Id = 1 + } + } + + $Global:SdnDiagnostics.Config.UseSSL = $true + $Global:SdnDiagnostics.Config.Port = 5987 + try { + New-PSRemotingSession -ComputerName 'DVLAB-S1-N01' + + Should -Invoke New-PSSession -Times 1 -ParameterFilter { + $Port -eq 5987 -and $UseSSL -eq $true + } + } + finally { + $Global:SdnDiagnostics.Config.UseSSL = $false + $Global:SdnDiagnostics.Config.Port = 0 + } + } + } + } +}