Public/Invoke-CCMCommand.ps1
function Invoke-CCMCommand { <# .SYNOPSIS Invoke commands remotely via Win32_Process:CreateProcess, or Invoke-Command .DESCRIPTION This function is used as part of the PSCCMClient Module. It's purpose is to allow commands to be execute remotely, while also automatically determining the best, or preferred method of invoking the command. Based on the type of connection that is passed, whether a CimSession, PSSession, or Computername with a ConnectionPreference, the command will be executed remotely by either using the CreateProcess Method of the Win32_Process CIM Class, or it will use Invoke-Command. .PARAMETER ScriptBlock The ScriptBlock that should be executed remotely .PARAMETER FunctionsToLoad A list of functions to load into the remote command exeuction. For example, you could specify that you want to load "Get-CustomThing" into the remote command, as you've already written the function and want to use it as part of the scriptblock that will be remotely executed. .PARAMETER ArgumentList The list of arguments that will be pass into the script block .PARAMETER Timeout The time in milliseconds after which the NamedPipe will timeout. The NamedPipe connection is used in the CimSession parameter set, as this is how the object is returned from the remote command. .PARAMETER CimSession Provides CimSessions to invoke the specified scriptblock on .PARAMETER ComputerName Provides computer names to invoke the specified scriptblock on .PARAMETER PSSession Provides PSSessions to invoke the specified scriptblock on .PARAMETER ConnectionPreference Determines if the 'Get-CCMConnection' function should check for a PSSession, or a CIMSession first when a ComputerName is passed to the function. This is ultimately going to result in the function running faster. The typical use case is when you are using the pipeline. In the pipeline scenario, the 'ComputerName' parameter is what is passed along the pipeline. The 'Get-CCMConnection' function is used to find the available connections, falling back from the preference specified in this parameter, to the the alternative (eg. you specify, PSSession, it falls back to CIMSession), and then falling back to ComputerName. Keep in mind that the 'ConnectionPreference' also determines what type of connection / command the ComputerName parameter is passed to. .EXAMPLE C:\PS> Invoke-CCMCommand -ScriptBlock { 'Testing This' } -ComputerName Workstation123 Would return the string 'Testing This' which was executed on the remote machine Workstation123 .EXAMPLE C:\PS> function Test-This { 'Testing This' } Invoke-CCMCommand -Scriptblock { Test-This } -FunctionsToLoad Test-This -ComputerName Workstation123 Would load the custom Test-This function into the scriptblock, and execute it. This would return the 'Testing This' string, based on the function being executed remotely on Workstation123. .NOTES FileName: Invoke-CCMCommand.ps1 Author: Cody Mathis Contact: @CodyMathis123 Created: 2020-02-12 Updated: 2020-03-02 #> [CmdletBinding(DefaultParameterSetName = 'ComputerName')] param ( [Parameter(Mandatory = $true)] [scriptblock]$ScriptBlock, [Parameter(Mandatory = $false)] [string[]]$FunctionsToLoad, [Parameter(Mandatory = $false)] [object[]]$ArgumentList, [Parameter(Mandatory = $false, ParameterSetName = 'CimSession')] [ValidateRange(1000, 900000)] [int32]$Timeout = 120000, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'CimSession')] [Microsoft.Management.Infrastructure.CimSession[]]$CimSession, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ComputerName')] [Alias('Connection', 'PSComputerName', 'PSConnectionName', 'IPAddress', 'ServerName', 'HostName', 'DNSHostName')] [string[]]$ComputerName = $env:ComputerName, [Parameter(Mandatory = $false, ParameterSetName = 'PSSession')] [Alias('Session')] [System.Management.Automation.Runspaces.PSSession[]]$PSSession, [Parameter(Mandatory = $false, ParameterSetName = 'ComputerName')] [ValidateSet('CimSession', 'PSSession')] [string]$ConnectionPreference = 'CimSession' ) begin { $HelperFunctions = switch ($PSBoundParameters.ContainsKey('FunctionsToLoad')) { $true { Convert-FunctionToString -FunctionToConvert $FunctionsToLoad } } $ConnectionChecker = switch ($PSCmdlet.ParameterSetName) { 'ComputerName' { $ConnectionPreference } default { $PSCmdlet.ParameterSetName } } switch ($ConnectionChecker) { 'CimSession' { $invokeCommandSplat = @{ ClassName = 'Win32_Process' MethodName = 'Create' } $PipeName = [guid]::NewGuid().Guid $ArgList = switch ($PSBoundParameters.ContainsKey('ArgumentList')) { $true { $SupportFunctionsToConvert = 'ConvertTo-Base64StringFromObject', 'ConvertFrom-CliXml', 'ConvertFrom-Base64ToObject' $PassArgList = $true ConvertTo-Base64StringFromObject -inputObject $ArgumentList } $false { $SupportFunctionsToConvert = 'ConvertTo-Base64StringFromObject' $PassArgList = $false [string]::Empty } } $SupportFunctions = Convert-FunctionToString -FunctionToConvert $SupportFunctionsToConvert $ScriptBlockString = [string]::Format(@' {0} {2} $namedPipe = New-Object System.IO.Pipes.NamedPipeServerStream "{1}", "Out" $namedPipe.WaitForConnection() $streamWriter = New-Object System.IO.StreamWriter $namedPipe $streamWriter.AutoFlush = $true $ScriptBlock = {{ {3} }} $TempResultPreConversion = switch([bool]${4}) {{ $true {{ $ScriptBlock.Invoke((ConvertFrom-Base64ToObject -inputString {5})) }} $false {{ $ScriptBlock.Invoke() }} }} $results = ConvertTo-Base64StringFromObject -inputObject $TempResultPreConversion $streamWriter.WriteLine("$($results)") $streamWriter.dispose() $namedPipe.dispose() '@ , $SupportFunctions, $PipeName, $HelperFunctions, $ScriptBlock, $PassArgList, $ArgList) $scriptBlockPreEncoded = [scriptblock]::Create($ScriptBlockString) $byteCommand = [System.Text.encoding]::UTF8.GetBytes($scriptBlockPreEncoded) $encodedScriptBlock = [convert]::ToBase64string($byteCommand) $invokeCommandSplat['Arguments'] = @{ CommandLine = [string]::Format("powershell.exe (invoke-command ([scriptblock]::Create([system.text.encoding]::UTF8.GetString([System.convert]::FromBase64string('{0}')))))", $encodedScriptBlock) } } 'PSSession' { $ScriptBlockString = [string]::Format(@' {0} {1} '@ , $HelperFunctions, $ScriptBlock) $FullScriptBlock = [scriptblock]::Create($ScriptBlockString) $InvokeCommandSplat = @{ ScriptBlock = $FullScriptBlock } switch ($PSBoundParameters.ContainsKey('ArgumentList')) { $true { $invokeCommandSplat['ArgumentList'] = $ArgumentList } } } } } process { foreach ($Connection in (Get-Variable -Name $PSCmdlet.ParameterSetName -ValueOnly -Scope Local)) { $getConnectionInfoSplat = @{ $PSCmdlet.ParameterSetName = $Connection } $ConnectionInfo = Get-CCMConnection @getConnectionInfoSplat -Prefer $ConnectionChecker $Computer = $ConnectionInfo.ComputerName $connectionSplat = $ConnectionInfo.connectionSplat switch ($ConnectionChecker) { 'CimSession' { $null = Invoke-CimMethod @invokeCommandSplat @connectionSplat $namedPipe = New-Object System.IO.Pipes.NamedPipeClientStream $Computer, "$($PipeName)", "In" $namedPipe.Connect($timeout) $streamReader = New-Object System.IO.StreamReader $namedPipe while ($null -ne ($data = $streamReader.ReadLine())) { $tempData = $data } $streamReader.dispose() $namedPipe.dispose() if (-not [string]::IsNullOrWhiteSpace($tempData)) { ConvertFrom-Base64ToObject -inputString $tempData } } 'PSSession' { Invoke-Command @InvokeCommandSplat @connectionSplat } } } } } |