-
-
Save Robert-LTH/7423e418aab033d114d7c8a2df99246b to your computer and use it in GitHub Desktop.
<# | |
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 | |
} |
"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.
What?!?! You are a super hero my friend
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
Try using -InputParameter as the example states how it should be used
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.
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
Thank you again for all your work on this I would not have gotten this without you