Skip to content

Instantly share code, notes, and snippets.

@Robert-LTH
Last active September 3, 2023 15:56
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Robert-LTH/7423e418aab033d114d7c8a2df99246b to your computer and use it in GitHub Desktop.
Save Robert-LTH/7423e418aab033d114d7c8a2df99246b to your computer and use it in GitHub Desktop.
Simple script to use the new sccm feature called "Run Script". The builtin Invoke-CMScript does not accept parameters.
<#
Thank you CodyMathis123 (https://github.com/CodyMathis123) for adding ParameterSets to do it the right way!
Updated 2022-08-22 thanks to Bryan Dam who noticed that the XML definition was changed and Type became DataType.
Also renamed function because SCCM is the old name of the product.
#>
<#
.SYNOPSIS
Run a script with custom parameters over WMI in ConfigMgr
.DESCRIPTION
Invoke scripts with custom parameters over WMI in ConfigMgr. Since the Cmdlet which is shipped with ConfigMgr isn't able to pass parameters to the script I had to write my own version which was able to do it.
.NOTES
This should be done using the AdminService but sometimes we need the legacy stuff.
.LINK
https://github.com/Robert-LTH
.EXAMPLE
Invoke-SCCMRunScript -SiteServer . -Namespace root\SMS\Site_TST-ScriptName 'ParameterTest' -TargetResourceIDs @(12345) -InputParameters @(@{Name='Param1';Type='System.String';Value='Parameter Value'})
#>
function Invoke-ConfigMgrRunScript {
param(
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]$SiteServer,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]$Namespace,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]$ScriptName,
[Array]$InputParameters = @(),
[parameter(Mandatory = $true, ParameterSetName = 'ByCollectionID')]
[string]$TargetCollectionID = "",
[parameter(Mandatory = $true, ParameterSetName = 'ByResourceID')]
[Array]$TargetResourceIDs = @()
)
# if something goes wrong, we want to stop!
$ErrorActionPreference = "Stop"
# Get the script
$Script = [wmi](Get-WmiObject -class SMS_Scripts -Namespace $Namespace -ComputerName $SiteServer -Filter "ScriptName = '$ScriptName' AND ApprovalState = 3").__PATH
if (-not $Script) {
throw "Could not find script with name '$ScriptName' (Maybe the script isn't approved yet?)"
}
# Parse the parameter definition
$Parameters = [xml]([string]::new([Convert]::FromBase64String($Script.ParamsDefinition)))
$Parameters.ScriptParameters.ChildNodes | % {
# In the case of a missing required parameter, bail!
if ($_.IsRequired -and $InputParameters.Count -lt 1) {
throw "Script 'ScriptName' has required parameters but no parameters was passed."
}
if ($_.Name -notin $InputParameters.Name) {
throw "Parameter '$($_.Name)' has not been passed in InputParamters!"
}
}
# GUID used for parametergroup
$ParameterGroupGUID = $(New-Guid)
if ($InputParameters.Count -le 0) {
# If no ScriptParameters: <ScriptParameters></ScriptParameters> and an empty hash
$ParametersXML = "<ScriptParameters></ScriptParameters>"
$ParametersHash = ""
}
else {
foreach ($Parameter in $InputParameters) {
$InnerParametersXML = "$InnerParametersXML<ScriptParameter ParameterGroupGuid=`"$ParameterGroupGUID`" ParameterGroupName=`"PG_$ParameterGroupGUID`" ParameterName=`"$($Parameter.Name)`" ParameterDataType=`"$($Parameter.DataType)`" ParameterValue=`"$($Parameter.Value)`"/>"
}
$ParametersXML = "<ScriptParameters>$InnerParametersXML</ScriptParameters>"
$SHA256 = [System.Security.Cryptography.SHA256Cng]::new()
$Bytes = ($SHA256.ComputeHash(([System.Text.Encoding]::Unicode).GetBytes($ParametersXML)))
$ParametersHash = ($Bytes | ForEach-Object ToString X2) -join ''
}
$RunScriptXMLDefinition = "<ScriptContent ScriptGuid='{0}'><ScriptVersion>{1}</ScriptVersion><ScriptType>{2}</ScriptType><ScriptHash ScriptHashAlg='SHA256'>{3}</ScriptHash>{4}<ParameterGroupHash ParameterHashAlg='SHA256'>{5}</ParameterGroupHash></ScriptContent>"
$RunScriptXML = $RunScriptXMLDefinition -f $Script.ScriptGuid,$Script.ScriptVersion,$Script.ScriptType,$Script.ScriptHash,$ParametersXML,$ParametersHash
# Get information about the class instead of fetching an instance
# WMI holds the secret of what parameters that needs to be passed and the actual order in which they have to be passed
$MC = [WmiClass]"\\$SiteServer\$($Namespace):SMS_ClientOperation"
# Get the parameters of the WmiMethod
$MethodName = 'InitiateClientOperationEx'
$InParams = $MC.psbase.GetMethodParameters($MethodName)
# Information about the script is passed as the parameter 'Param' as a BASE64 encoded string
$InParams.Param = ([Convert]::ToBase64String(([System.Text.Encoding]::UTF8).GetBytes($RunScriptXML)))
# Hardcoded to 0 in certain DLLs
$InParams.RandomizationWindow = "0"
# If we are using a collection, set it. TargetCollectionID can be empty string: ""
$InParams.TargetCollectionID = $TargetCollectionID
# If we have a list of resources to run the script on, set it. TargetResourceIDs can be an empty array: @()
# Criteria for a "valid" resource is IsClient=$true and IsBlocked=$false and IsObsolete=$false and ClientType=1
$InParams.TargetResourceIDs = $TargetResourceIDs
# Run Script is type 135
$InParams.Type = "135"
# Everything should be ready for processing, invoke the method!
$R = $MC.InvokeMethod($MethodName, $InParams, $null)
# The result contains the client operation id of the execution
$R
}
@bryandam
Copy link

@Robert-LTH: Do you remember how you found the RunScriptXMLDefinition? The script used to work but when I tried it recently it fails with 'Generic Error' coming from the WMI Method call. I've tried finding the source of that myself but haven't had any luck.

@bryandam
Copy link

bryandam commented Aug 11, 2022

Must have been rubber ducking it: found it in vSMS_ScriptsExecutionTask.ClientNotificationMessage
Looks like they've added a new ScriptParameter property ParameterDataType that is not optional.
Ex:

<ScriptParameter 
        ParameterName="Name"
	ParameterDataType="System.String"
	ParameterValue="Value"
/>

@Robert-LTH
Copy link
Author

It was a while ago and i think i found most of what I needed from RE:ing the DLLs from the admin console.

Nice work! I will update the gist when i have a chance.

@tobor88
Copy link

tobor88 commented Aug 15, 2023

Thank you very much for doing all the hard work here.

Below is the CIM way to do the same thing for anyone who needs it and a few modernizations.

I realized the Get-WMIObject is going is required for two purposes. When you get the SMS_Scripts SCCM script from CIM there is no value by default in $CIMScript.Script or $CIMScript.ParamsDefinition.

These values both require a base64 encoded entry. The ParamsDefinintion I have in this function become manually built, however, it is sloppy because the Default value and IsRequired values have to be assumed. Until these values can be obtained without WMI I have this using Get-WmiObject also. You can set IsRequired in the XML but it does not affect the execution of the SCCM script. The 'Script' value appears to be encrypted before it is base64 encoded. Rather than go through all that trouble you would think there would be an existence of the needed value somewhere else. I have not found it yet but I hope too. I will update this if I ever do

Function Invoke-SccmRunScript {
<#
.SYNOPSIS
This cmdlet can be used to execute an SCCM script using CIM and does not use the ConfigurationManager PowerShell module


.DESCRIPTION
Execute an approved SCCM script against a Device Collection or an array of Device Resource IDs


.PARAMETER ScriptName
Define the name of the approved SCCM script you want to execute

.PARAMETER ScriptGuid
Define the GUID identifier of the approved SCCM script you want to execute

.PARAMETER InputParameter
Define the parameter and the value for it you wish to pass into the SCCM script

.PARAMETER CollectionID
Define the Device Collection ID you want to execute your script against

.PARAMETER ResourceID
Define the device Resource ID's you want to execute your script against

.PARAMETER SiteServer
Define your SCCM server

.PARAMETER UseSSL
Connect to your -SiteServer for SCCM through an SSL connection

.PARAMETER SkipCACheck
Skip checking the trust of the Root Certificate Authority on the destinations certificate

.PARAMETER SkipCNCheck
Ignore the CN/Subject Name value of the destination certificate

.PARAMETER SkipRevocationCheck
Skip checking the CRL for revoked certificates

.PARAMETER Credential
Enter the SCCM credentials to authenticate with


.EXAMPLE
PS> Invoke-SccmRunScript -ScriptName "Test Script" -ResourceID 11111111,22222222,33333333 -SiteServer sccm.domain.com -Credential $LiveCred
# This example executes the SCCM script "Test Script" against the devices with resource IDs of 11111111, 222222222, and 33333333 on the sccm.domain.com SCCM server using a normal CIM connection

.EXAMPLE
PS> Invoke-SccmRunScript -ScriptName "Test Script" -CollectonID 12345678 -SiteServer sccm.domain.com -UseSSL -Credential $LiveCred
# This example executes the SCCM script "Test Script" against collection ID 12345678 on the sccm.domain.com SCCM server using an SSL connection

.EXAMPLE
PS> Invoke-SccmRunScript -ScriptGuid "47bba6a5-c59c-49b8-b0a7-d36f903984e1" -CollectonID 12345678 -SiteServer sccm.domain.com -UseSSL -SkipCACheck -SkipCNCheck -SkipRevocationCheck -Credential $LiveCred
# This example executes the SCCM script 47bba6a5-c59c-49b8-b0a7-d36f903984e1 against collection ID 12345678 on the sccm.domain.com SCCM server using an SSL connection where all certificate checks are ignored

.NOTES
Authors: Robert Johnsson, Robert H. Osborne
Contact: rosborne@osbornepro.com


.LINK
https://osbornepro.com/
https://twitter.com/johnsson_r


.INPUTS
None


.OUTPUTS
None
#>
    [OutputType([System.Object])]
    [CmdletBinding(DefaultParameterSetName="ScriptGuid")]
        param(
            [Parameter(
                ParameterSetName='ScriptName',
                Mandatory=$False
            )]  # End Parameter
            [ValidateNotNullOrEmpty()]
            [String]$ScriptName,
 
            [Parameter(
                ParameterSetName='ScriptGuid',
                Mandatory=$False
            )]  # End Parameter
            [ValidatePattern('(\{|\()?[A-Za-z0-9]{4}([A-Za-z0-9]{4}\-?){4}[A-Za-z0-9]{12}(\}|\()?')]
            [String]$ScriptGuid,
 
            [Parameter(
                Mandatory=$False,
                HelpMessage="Define your input parameter name, type and value `n[EXAMPLE] @(@{Name='GetMissingUpdates';Type='System.String';Value='True'},@{Name='ApprovedSCCMUpdates';Type='System.String';Value='True'}) `n[VALUE] "
            )]  # End Parameter
            [Array]$InputParameter,
 
            [Parameter(
                Mandatory=$False
            )]  # End Parameter
            [Alias('TargetCollectionID')]
            [String]$CollectionID,
 
            [Parameter(
                Mandatory=$False
            )]  # End Parameter
            [Alias('TargetResourceID')]
            [Array]$ResourceID = [UInt32[]]@(),
 
            [Parameter(
                Mandatory=$False,
                HelpMessage="Define the SCCM server containing your script `n[EXAMPLE] sccm.domain.com `n[INPUT] "
            )]  # End Parameter
            [ValidateNotNullOrEmpty()]
            [String]$SiteServer,
 
            [Parameter(
                Mandatory=$False
            )]  # End Parameter
            [Switch]$UseSSL,
 
            [Parameter(
                Mandatory=$False
            )]  # End Parameter
            [Switch]$SkipCACheck,
 
            [Parameter(
                Mandatory=$False
            )]  # End Parameter
            [Switch]$SkipCNCheck,
 
            [Parameter(
                Mandatory=$False
            )]  # End Parameter
            [Switch]$SkipRevocationCheck,
 
            [ValidateNotNull()]
            [System.Management.Automation.PSCredential]
            [System.Management.Automation.Credential()]
            $Credential = [System.Management.Automation.PSCredential]::Empty
        )  # End param
 
    Write-Verbose -Message "[v] $(Get-Date -Format 'MM-dd-yyyy hh:mm:ss') Parameter Set Names: $($PSCmdlet.ParameterSetName)"
    If (!($PSBoundParameters.ContainsKey('CollectionID')) -and !($PSBoundParameters.ContainsKey('ResourceID'))) {
 
        Throw "[x] $(Get-Date -Format 'MM-dd-yyyy hh:mm:ss') You are required to define either -ResourceID or -CollectionID"
 
    }  # End If
 
    $ErrorActionPreference = "Stop"
    $ParameterGroupGUID = [System.Guid]::NewGuid()
    $ParametersHash = ""
    $ParametersXML = "<ScriptParameters></ScriptParameters>"
    $ParamsDefinition = "<?xml version=`"1.0`" encoding=`"utf-16`"?><ScriptParameters SchemaVersion=`"1`"></ScriptParameters>"
 
    If ($PSBoundParameters.ContainsKey('SiteServer')) {
 
        Try {
 
            Write-Verbose -Message "[v] $(Get-Date -Format 'MM-dd-yyyy hh:mm:ss') Determining Site Code for Site server: '$($SiteServer)'"
            $SccmCimSession = New-CimSession -Credential $Credential -ComputerName $SiteServer -SessionOption (New-CimSessionOption -UseSSL:$UseSSL.IsPresent -SkipCACheck:$SkipCACheck.IsPresent -SkipCNCheck:$SkipCNCheck.IsPresent -SkipRevocationCheck:$SkipRevocationCheck.IsPresent) -OperationTimeoutSec 15 -Verbose:$False
            $SiteCodeObjects = Get-CimInstance -CimSession $SccmCimSession -Namespace "Root\SMS" -ClassName SMS_ProviderLocation -ErrorAction Stop -Verbose:$False
 
            ForEach ($SiteCodeObject in $SiteCodeObjects) {
 
                If ($SiteCodeObject.ProviderForLocalSite -eq $True) {
 
                    $SiteCode = $SiteCodeObject.SiteCode
                    Write-Verbose -Message "[v] $(Get-Date -Format 'MM-dd-yyyy hh:mm:ss') Site Code: $SiteCode"
                    Break
 
                }  # End If
 
            }  # End ForEach
 
        } Catch [System.UnauthorizedAccessException] {
 
            Throw "[x] $(Get-Date -Format 'MM-dd-yyyy hh:mm:ss') Access denied"
 
        } Catch [System.Exception] {
 
            Throw "[x] $(Get-Date -Format 'MM-dd-yyyy hh:mm:ss') Unable to determine Site Code"
 
        }  # End Try Catch Catch
 
        $Namespace = "Root\SMS\Site_$SiteCode"
        If ($PSCmdlet.ParameterSetName.Contains('ScriptName')) {
 
            $CIMScript = Get-CimInstance -CimSession $SccmCimSession -ClassName SMS_Scripts -Namespace $Namespace -Filter "ScriptName = '$ScriptName' AND ApprovalState = 3" -Verbose:$False
            $CIMScriptInfo = $CIMScript | Get-CimInstance -CimSession $SccmCimSession -Verbose:$False
 
        } ElseIf ($PSCmdlet.ParameterSetName.Contains('ScriptGuid')) {
 
            $CIMScript = Get-CimInstance -CimSession $SccmCimSession -ClassName SMS_Scripts -Namespace $Namespace -Filter "ScriptGuid = '$ScriptGuid' AND ApprovalState = 3" -Verbose:$False
            $CIMScriptInfo = $CIMScript | Get-CimInstance -CimSession $SccmCimSession -Verbose:$False
 
        } Else {
 
            Throw "[x] $(Get-Date -Format 'MM-dd-yyyy hh:mm:ss') Issue obtaining the parameter set name for the invoked function"
 
        }  # End If ElseIf Else
 
    } Else {
 
        Try {
 
            Write-Verbose -Message "[v] $(Get-Date -Format 'MM-dd-yyyy hh:mm:ss') Determining Site Code for Site server: '$($SiteServer)'"
            $SiteCodeObjects = Get-CimInstance -Namespace "Root\SMS" -ClassName SMS_ProviderLocation -ErrorAction Stop -Verbose:$False
            ForEach ($SiteCodeObject in $SiteCodeObjects) {
 
                If ($SiteCodeObject.ProviderForLocalSite -eq $True) {
 
                    $SiteCode = $SiteCodeObject.SiteCode
                    Write-Verbose -Message "[v] $(Get-Date -Format 'MM-dd-yyyy hh:mm:ss') Site Code: $SiteCode"
                    Break
 
                }  # End If
 
            }  # End ForEach

        } Catch [System.UnauthorizedAccessException] {
 
            Throw "[x] $(Get-Date -Format 'MM-dd-yyyy hh:mm:ss') Access denied"
 
        } Catch [System.Exception] {
 
            Throw "[x] $(Get-Date -Format 'MM-dd-yyyy hh:mm:ss') Unable to determine Site Code"
 
        }  # End Try Catch Catch
 
        $Namespace = "Root\SMS\Site_$SiteCode"
        If ($PSCmdlet.ParameterSetName.Contains('ScriptName')) {
 
            $CIMScript = Get-CimInstance -ClassName SMS_Scripts -Namespace $Namespace -Filter "ScriptName = '$ScriptName' AND ApprovalState = 3" -Verbose:$False
            $CIMScriptInfo = $CIMScript | Get-CimInstance -Verbose:$False
 
        } ElseIf ($PSCmdlet.ParameterSetName.Contains('ScriptGuid')) {
 
            $CIMScript = Get-CimInstance -ClassName SMS_Scripts -Namespace $Namespace -Filter "ScriptGuid = '$ScriptGuid' AND ApprovalState = 3" -Verbose:$False
            $CIMScriptInfo = $CIMScript | Get-CimInstance -Verbose:$False
 
        } Else {
 
            Throw "[x] $(Get-Date -Format 'MM-dd-yyyy hh:mm:ss') Issue obtaining the parameter set name for the invoked function"
 
        }  # End If ElseIf Else
 
    }  # End If Else
 
    If ($InputParameter) {
 
        $InnerParametersXML = $Null
        $InnerParamsDefinitionXML = $Null
 
        ForEach ($Parameter in $InputParameter) {
 
            $InnerParametersXML = "$InnerParametersXML<ScriptParameter ParameterGroupGuid=`"$ParameterGroupGUID`" ParameterGroupName=`"PG_$ParameterGroupGUID`" ParameterName=`"$($Parameter.Name)`" ParameterDataType=`"$($Parameter.Type)`" ParameterValue=`"$($Parameter.Value)`"/>"
            $InnerParamsDefinitionXML = "$InnerParamsDefinitionXML<ScriptParameter Name=`"$($Parameter.Name)`" FriendlyName=`"$($Parameter.Name)`" Type=`"$($Parameter.Type)`" Description=`"`" IsRequired=`"false`" IsHidden=`"false`" DefaultValue=`"`"><Validators /></ScriptParameter>"
 
        }  # End ForEach
 
        $ParamsDefinition = "<?xml version=`"1.0`" encoding=`"utf-16`"?><ScriptParameters SchemaVersion=`"1`">$InnerParamsDefinitionXML</ScriptParameters>"
        $ParametersXML = "<ScriptParameters>$InnerParametersXML</ScriptParameters>"
 
    }  # End If
 
    $SHA256 = New-Object -TypeName System.Security.Cryptography.SHA256Managed
    $Bytes = ($SHA256.ComputeHash(([System.Text.Encoding]::Unicode).GetBytes($ParametersXML)))
    $ParametersHash = ($Bytes | ForEach-Object ToString X2 -WhatIf:$False) -Join ''
 
    $CIMScript.Script = $CIMScriptInfo.Script
    $CIMScript.ParamsDefinition = [System.Convert]::ToBase64String($([System.Text.Encoding]::UTF8.GetBytes($ParamsDefinition.Trim())))
    # NOTE: The above can be used to manually build the base64 value for the CIM object parameter ParamsDefinition.
    # I can build this manually but its sloppy to do and assumes values for 'Default' and 'IsRequired' in the XML values
 
    $RunScriptXML = "<ScriptContent ScriptGuid='$($CIMScript.ScriptGuid)'><ScriptVersion>$($CIMScript.ScriptVersion)</ScriptVersion><ScriptType>$($CIMScript.ScriptType)</ScriptType><ScriptHash ScriptHashAlg='SHA256'>$($CIMScript.ScriptHash)</ScriptHash>$($ParametersXML)<ParameterGroupHash ParameterHashAlg='SHA256'>$($ParametersHash)</ParameterGroupHash></ScriptContent>"
    $CIMArguments = @{
        Param = ([Convert]::ToBase64String(([System.Text.Encoding]::UTF8).GetBytes($RunScriptXML)));
        RandomizationWindow = [Uint32]0;
        TargetCollectionID = $CollectionID;
        TargetResourceIDs = [UInt32[]]$ResourceID;
        Type = [UInt32]135;
    };
 
    If ($PSBoundParameters.ContainsKey('SiteServer')) {
 
        $ScriptExecResult = Invoke-CimMethod -CimSession $SccmCimSession -Namespace $Namespace -ClassName SMS_ClientOperation -MethodName InitiateClientOperationEx -Arguments $CIMArguments -Verbose:$False -WhatIf:$False
 
    } Else {
 
        $ScriptExecResult = Invoke-CimMethod -Namespace $Namespace -ClassName SMS_ClientOperation -MethodName InitiateClientOperationEx -Arguments $CIMArguments -Verbose:$False -WhatIf:$False
 
    }  # End If Else
 
    New-Object -TypeName PSCustomObject -Property @{
        ScriptName=$CIMScript.ScriptName;
        ScriptGuid=$CIMScript.ScriptGuid;
        OperationID=$ScriptExecResult.OperationID;
        ExecutionResult=$(Switch ($ScriptExecResult.ReturnValue) { 0 { 'Successful' } Default { "Failed with return value: $($ScriptExecResult.ReturnValue)" }});
    }  # End New-Object -Property

    $ErrorActionPreference = "Continue"

}  # End Function Invoke-SccmRunScript

Thank you again for all your work on this I would not have gotten this without you

@Robert-LTH
Copy link
Author

"I realized the Get-WMIObject is going is required for two purposes. When you get the SMS_Scripts SCCM script from CIM there is no value by default in $CIMScript.Script or $CIMScript.ParamsDefinition."
I think this is because the properties are defined as lazy, try piping to Get-CimInstance once more like this:

$CIMScript = Get-CimInstance -ClassName SMS_Scripts -Namespace $Namespace -Filter "ScriptName = '$ScriptName' AND ApprovalState = 3" -Verbose:$False | Get-CimInstance

Its been a while but i have a vague memory of thats how you do it.

@tobor88
Copy link

tobor88 commented Aug 26, 2023

What?!?! You are a super hero my friend

@tobor88
Copy link

tobor88 commented Sep 1, 2023

I have update my post above to use the latest code utilzing CIM only :-)
$CIMScript.ParamsDefinition is the only problem child still left. I will work more on making this work by defining -InputParameter in the cmdlet is my plan

@Robert-LTH
Copy link
Author

Try using -InputParameter as the example states how it should be used

@tobor88
Copy link

tobor88 commented Sep 3, 2023

That is the plan. The trouble I picture having that I have not really looked at yet is getting the default value and mandatory values when building that string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment