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
}
@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