Private/Stop-OpenSshServer.ps1
|
Set-StrictMode -Version Latest function Get-OpenSshServerStopVersion { '0.3.1' } function Get-OpenSshServerStopHelp { @" Stop-OpenSshServer.ps1 Usage: .\Stop-OpenSshServer.ps1 [options] Options: -Force Force stop the sshd service. -Yes Skip confirmation prompts when elevation is required. -DryRun Preview actions without applying changes. -Port <int> TCP port to verify sshd is no longer listening (default: 22). -Json Emit machine-readable JSON output only. -Quiet Suppress non-error output. -Trace Emit verbose diagnostic output. -Version Print version and exit. -Help Print this help and exit. Notes: - Use -WhatIf to simulate changes (PowerShell common parameter). - Use -Confirm to force confirmation prompts (PowerShell common parameter). "@ } function Test-IsAdmin { $current = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($current) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Confirm-AutoFix { param( [Parameter(Mandatory)] [string]$Message, [Parameter(Mandatory)] [bool]$Yes ) if ($Yes) { return $true } if (-not [Environment]::UserInteractive) { return $false } $answer = Read-Host "$Message (Y/n)" if ([string]::IsNullOrWhiteSpace($answer)) { return $true } return $answer -match '^(y|yes)$' } function Get-InvocationArgumentList { param( [Parameter(Mandatory)] [hashtable]$BoundParameters, [Parameter(Mandatory)] [string[]]$ExcludeKeys ) $argumentList = @() foreach ($key in $BoundParameters.Keys) { if ($ExcludeKeys -contains $key) { continue } $value = $BoundParameters[$key] if ($value -is [switch]) { if ($value.IsPresent) { $argumentList += "-$key" } } elseif ($value -is [bool]) { if ($value) { $argumentList += "-$key" } } else { $argumentList += "-$key" $argumentList += "$value" } } return $argumentList } $script:OpenSshStopDependencies = @{ GetCommand = { param($Name) Get-Command -Name $Name -ErrorAction SilentlyContinue } GetService = { param($Name) Get-Service -Name $Name -ErrorAction Stop } StopService = { param($Name, $Force) Stop-Service -Name $Name -Force:$Force -ErrorAction Stop } GetNetTcpConnection = { param($Port) Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue } GetProcess = { param($Id) Get-Process -Id $Id -ErrorAction Stop } IsAdmin = { Test-IsAdmin } Elevate = { param($ExePath, $ArgumentList) Start-Process -FilePath $ExePath -ArgumentList $ArgumentList -Verb RunAs | Out-Null } RunSudo = { param($ExePath, $ArgumentList) & sudo -- $ExePath @ArgumentList } } function Get-StopResult { [pscustomobject]@{ version = Get-OpenSshServerStopVersion status = 'success' stopped = $false checks = @() actions = @() warnings = @() errors = @() } } function Add-ResultItem { param( [Parameter(Mandatory)] [object]$Result, [Parameter(Mandatory)] [string]$Collection, [Parameter(Mandatory)] [object]$Item ) $Result.$Collection += $Item } function Invoke-OpenSshServerStop { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param( [switch]$Force, [switch]$Yes, [switch]$DryRun, [ValidateRange(1, 65535)] [int]$Port = 22, [switch]$Json, [switch]$Quiet, [switch]$Trace, [switch]$Version, [switch]$Help, [Parameter(DontShow)] [hashtable]$Dependencies ) if ($Version) { Write-Output (Get-OpenSshServerStopVersion) return } if ($Help) { Write-Output (Get-OpenSshServerStopHelp) return } if ($Json) { $Quiet = $true } if ($Trace) { $VerbosePreference = 'Continue' } if ($DryRun) { $WhatIfPreference = $true } $result = Get-StopResult $deps = if ($Dependencies) { $Dependencies } else { $script:OpenSshStopDependencies } $isWindowsPlatform = [System.Environment]::OSVersion.Platform -eq 'Win32NT' $null = $Yes $invocationBoundParameters = $PSBoundParameters function Write-StopLog { param( [Parameter(Mandatory)] [string]$Message, [ValidateSet('Info', 'Warning', 'Error', 'Verbose')] [string]$Level = 'Info' ) if ($Quiet -and $Level -ne 'Error') { return } switch ($Level) { 'Info' { Write-Information $Message -InformationAction Continue } 'Warning' { Write-Warning $Message } 'Error' { Write-Error $Message } 'Verbose' { Write-Verbose $Message } } } function Register-Check { param( [string]$Id, [string]$Status, [string]$Message, [string]$Remediation ) Add-ResultItem -Result $result -Collection 'checks' -Item ([pscustomobject]@{ id = $Id status = $Status message = $Message remediation = $Remediation }) } function Register-Action { param( [string]$Action, [string]$Details ) Add-ResultItem -Result $result -Collection 'actions' -Item ([pscustomobject]@{ action = $Action details = $Details }) } function Register-Error { param( [string]$Id, [string]$Message, [string]$Remediation ) $result.status = 'error' Add-ResultItem -Result $result -Collection 'errors' -Item ([pscustomobject]@{ id = $Id message = $Message remediation = $Remediation }) Write-StopLog -Level 'Error' -Message $Message if ($Remediation) { Write-StopLog -Level 'Error' -Message "Remediation: $Remediation" } } function Register-Warning { param( [string]$Id, [string]$Message ) Add-ResultItem -Result $result -Collection 'warnings' -Item ([pscustomobject]@{ id = $Id message = $Message }) Write-StopLog -Level 'Warning' -Message $Message } function Request-Elevation { param( [Parameter(Mandatory)] [string]$Reason ) if ($WhatIfPreference) { Register-Error -Id 'requires_admin' -Message "Administrator privileges required to $Reason." -Remediation 'Run in an elevated PowerShell session and retry.' return $false } $confirmMessage = "Administrator privileges required to $Reason. Relaunch as Administrator now?" if (-not (Confirm-AutoFix -Message $confirmMessage -Yes:$Yes)) { Register-Error -Id 'requires_admin' -Message "Administrator privileges required to $Reason." -Remediation 'Start PowerShell as Administrator and rerun.' return $false } $scriptPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'Stop-OpenSshServer.ps1' $exePath = if (& $deps.GetCommand 'pwsh') { 'pwsh' } else { 'powershell' } $baseArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $scriptPath) + (Get-InvocationArgumentList -BoundParameters $invocationBoundParameters -ExcludeKeys @('Dependencies')) $usedSudo = $false if (& $deps.GetCommand 'sudo') { & $deps.RunSudo $exePath $baseArgs if ($LASTEXITCODE -eq 0) { $usedSudo = $true Register-Action -Action 'elevate' -Details 'Relaunched with sudo.' Register-Warning -Id 'relaunching_elevated' -Message 'Elevated command launched via sudo.' } else { Register-Warning -Id 'sudo_failed' -Message 'sudo is unavailable or failed; falling back to a new elevated window.' } } if (-not $usedSudo) { $argList = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-NoExit', '-File', $scriptPath) + (Get-InvocationArgumentList -BoundParameters $invocationBoundParameters -ExcludeKeys @('Dependencies')) & $deps.Elevate $exePath $argList Register-Action -Action 'elevate' -Details 'Relaunched with elevation.' Register-Warning -Id 'relaunching_elevated' -Message 'Opened a new elevated PowerShell window to continue operation.' } $script:ElevationRequested = $true throw 'ElevationRestarted' return $true } function Invoke-Check { param( [string]$Id, [string]$Description, [scriptblock]$Test, [string]$FailureMessage, [string]$Remediation ) try { $testResult = & $Test if ($testResult) { Register-Check -Id $Id -Status 'ok' -Message $Description -Remediation '' return } } catch { Write-StopLog -Level 'Verbose' -Message "Check '$Id' threw: $($_.Exception.Message)" } Register-Check -Id $Id -Status 'error' -Message $FailureMessage -Remediation $Remediation Register-Error -Id $Id -Message $FailureMessage -Remediation $Remediation throw $FailureMessage } if (-not $isWindowsPlatform) { Register-Error -Id 'unsupported_os' -Message "This script only supports Windows. Detected platform: $([System.Environment]::OSVersion.Platform)." -Remediation 'Run this script on Windows with OpenSSH Server installed.' return $result } $script:ElevationRequested = $false try { Invoke-Check -Id 'tcp_cmdlets' -Description 'NetTCPIP cmdlets are available.' -Test { return $null -ne (& $deps.GetCommand 'Get-NetTCPConnection') } -FailureMessage 'NetTCPIP cmdlets are unavailable (Get-NetTCPConnection missing).' -Remediation 'Install the NetTCPIP module or run on a Windows build that includes it.' Invoke-Check -Id 'sshd_service' -Description "OpenSSH Server service 'sshd' is registered." -Test { & $deps.GetService 'sshd' | Out-Null return $true } -FailureMessage "OpenSSH Server service 'sshd' is not installed." -Remediation "Install OpenSSH Server and ensure the 'sshd' service exists." $service = & $deps.GetService 'sshd' if ($service.Status -ne 'Running') { Register-Warning -Id 'sshd_not_running' -Message "OpenSSH Server service 'sshd' is already stopped." $result.stopped = $true } else { if (-not (& $deps.IsAdmin)) { $null = Request-Elevation -Reason 'stop the OpenSSH Server service' return $result } if ($PSCmdlet.ShouldProcess("sshd", 'Stop OpenSSH Server service')) { try { & $deps.StopService 'sshd' $Force Register-Action -Action 'sshd_stop' -Details 'Stopped OpenSSH Server service.' } catch { Register-Error -Id 'sshd_stop_failed' -Message "Failed to stop OpenSSH Server service: $($_.Exception.Message)" -Remediation 'Check service permissions and retry.' return $result } } $service = & $deps.GetService 'sshd' if ($service.Status -ne 'Stopped') { Register-Error -Id 'sshd_stop_failed' -Message "OpenSSH Server service 'sshd' is still running after stop attempt." -Remediation 'Check the OpenSSH operational log for errors.' return $result } $result.stopped = $true } Invoke-Check -Id 'sshd_listening' -Description "sshd is not listening on TCP port $Port." -Test { $listeners = & $deps.GetNetTcpConnection $Port if (-not $listeners) { return $true } foreach ($listener in $listeners) { try { $proc = & $deps.GetProcess $listener.OwningProcess if ($proc.ProcessName -eq 'sshd') { return $false } } catch { return $false } } return $true } -FailureMessage "sshd is still listening on TCP port $Port after stop." -Remediation 'Check for lingering sshd processes and terminate them if necessary.' Write-StopLog -Message 'OpenSSH Server is stopped.' -Level 'Info' } catch { if ($script:ElevationRequested) { return $result } if ($result.status -ne 'error') { $result.status = 'error' } } return $result } |