Updating SCCM Application OS Requirement Rules

As many of you are aware Windows 10 was released, you also know this means that there is a new OS type in the selection dialog. However this also means that if you use OS requirements on your applications you have to go update them to add in Windows 10. Which lets face it, if you have a thousand or so apps, you really don't want to do this all by hand. Since I was faced with this dilemma  I decided to try scripting it. I discovered that it is not as easy as one would expect. After lots of searching and even some help from a friendly PFE. We were able to create something that would get the job accomplished. That being said here are the useful portions of the script that I hope will help you create one of your own that will meet your needs. I will warn you this is not pretty and the full script triggered some crosseye results from my teammates.

Yes this is a very long and mostly code post.

# Load SCCM Assemblies
function Import-SCCMAssemblies
{
[CmdletBinding()]
    param
(
[Parameter(Mandatory = $false,
  ValueFromPipeline = $true,
  ValueFromPipelineByPropertyName = $true,
  ValueFromRemainingArguments = $true,
  HelpMessage = 'Path to the AdminConsole\Bin directory.')]
[Alias('Path')]
[System.String]
$ConsolePath = $Env:SMS_ADMIN_UI_PATH.Substring(0, $Env:SMS_ADMIN_UI_PATH.Length - 5)
)
Begin
{

}
Process
{
if (Test-Path $ConsolePath)
{
$files = Get-ChildItem "$ConsolePath\*.dll" -File
ForEach ($file in $files)
{
                   if ($file.Name -eq 'ServicingNode.dll') { continue }

Write-Verbose "Importing Assembly: $($file.FullName)"
$Assembly = [System.IO.Path]::Combine($ConsolePath, $file)
[void][Reflection.Assembly]::LoadFrom($Assembly)
}
}
else
{
sendl "$ConsolePath not found."
}
}
End
{

}
}

# Gets the Global Condition from WMI
function Get-SCCMGlobalCondition
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true,
  ValueFromPipeline = $true,
  ValueFromPipelineByPropertyName = $true,
  Position = 0)]
[Alias('ServerName')]
[Alias('SiteServer')]
[System.String]
$SCCMServer,
[Parameter(Mandatory = $true,
  ValueFromPipeline = $true,
  ValueFromPipelineByPropertyName = $true,
  Position = 1)]
[Alias('SiteCode')]
[System.String]
$SCCMSiteCode,
[Parameter(Mandatory = $true,
  ValueFromPipeline = $true,
  ValueFromPipelineByPropertyName = $true,
  Position = 2)]
[Alias('ConditionName')]
[System.String]
$GlobalConditionName
)
Begin
{

}
Process
{
$Filter = "LocalizedDisplayName = '$GlobalConditionName'"
$Condition = Get-WmiObject -ComputerName $SCCMServer -Namespace "root\sms\Site_$SCCMSiteCode" -Class SMS_GlobalCondition -Filter $Filter | Where IsLatest -eq $True
Write-Output $Condition
}
End
{

}

}

# Create new Global Condition Rule
Function New-SCCMGlobalConditionsRule
{
[CmdletBinding(SupportsShouldProcess = $True)]
Param (
[Parameter(Mandatory = $true, ValueFromPipeline = $True, ValueFromPipelinebyPropertyName = $True)]
[string]$SCCMServer,

[Parameter(Mandatory = $true, ValueFromPipeline = $True, ValueFromPipelinebyPropertyName = $True)]
[string]$SCCMSiteCode,

[Parameter(Mandatory = $true, ValueFromPipeline = $True, ValueFromPipelinebyPropertyName = $True)]
[string]$GlobalConditionName,

[Parameter(Mandatory = $true)]
[string]$Operator,

[Parameter(Mandatory = $true)]
[string]$Value
)

Process
{
$GlobalCondition = Get-SCCMGlobalCondition -SCCMServer $SCCMServer -SCCMSiteCode $SCCMSiteCode -GlobalConditionName $GlobalConditionName

$gcTmp = $GlobalCondition.ModelName.Split("/")
$gcScope = $gcTmp[0]
$gcLogicalName = $gcTmp[1]
$gcDataType = $GlobalCondition.DataType
$gcExpressionDataType = [Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.DataType]::GetDataTypeFromTypeName($gcDataType)

$arg = @(
$gcScope,
$gcLogicalName,
$gcExpressionDataType,
"$($gcLogicalName)_Setting_LogicalName",
([Microsoft.ConfigurationManagement.DesiredConfigurationManagement.ConfigurationItemSettingSourceType]::CIM)
)
$reqSetting = new-object  Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.GlobalSettingReference -ArgumentList $arg

$arg = @(
$Value,
$gcExpressionDataType
)
$reqValue = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.ConstantValue -ArgumentList $arg

$operands = new-object "Microsoft.ConfigurationManagement.DesiredConfigurationManagement.CustomCollection``1[[Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.ExpressionBase]]"
$operands.Add($reqSetting) | Out-Null
$operands.Add($reqValue) | Out-Null

$Expoperator = Invoke-Expression [Microsoft.ConfigurationManagement.DesiredConfigurationManagement.ExpressionOperators.ExpressionOperator]::$operator

if ($GlobalCondition.DataType -eq "OperatingSystem")
{
$operands = new-object "Microsoft.ConfigurationManagement.DesiredConfigurationManagement.CustomCollection``1[[Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.RuleExpression]]"
            $values = $value -split ";"            
           
foreach ($os in $values)
{
                   $operands.Add($os)
}

$arg = @(
$Expoperator,
$operands
)
$expression = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.OperatingSystemExpression -ArgumentList $arg

}
else
{
$arg = @(
$Expoperator,
$operands
)
$expression = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Expressions.Expression -ArgumentList $arg

}

$anno = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.Annotation
if ($GlobalCondition.DataType -eq "OperatingSystem")
{
# Annotation display name
$osvalues = $Value.Split(";")
$localizedOSNames = @()
foreach ($os in $osvalues) {
$Filter = "CI_UniqueId = '$os'"
$localizedOSNames += (Get-WmiObject -ComputerName $SCCMServer -Namespace "root\sms\Site_$SCCMSiteCode" -Class SMS_ConfigurationItem -Filter $Filter | Where IsLatest -eq $True).LocalizedDisplayName
}

$localizedDisplayName = "{"
foreach ($name in $localizedOSNames) { $localizedDisplayName += $name + ", " }
$localizedDisplayName = $localizedDisplayName.Substring(0, $localizedDisplayName.Length - 2)
$localizedDisplayName += "}"

if ($operator -eq "OneOf") {
$annodisplay = "Operating system One of $localizedDisplayName" }
else {
$annodisplay = "Operating system $operator $value"
}

}
else
{
$annodisplay = "$GlobalConditionName $operator $value"
}

$arg = @(
"DisplayName",
$annodisplay,
$null
)
$anno.DisplayName = new-object Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.LocalizableString -ArgumentList $arg

$arg = @(
("Rule_" + [Guid]::NewGuid().ToString()),
[Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.NoncomplianceSeverity]::None,
$anno,
$expression
)
$rule = New-Object "Microsoft.SystemsManagementServer.DesiredConfigurationManagement.Rules.Rule" -ArgumentList $arg


return $rule
}#End Process Block
}


!NOTICE!
On the following method, I limited it to just OS rules and then only desktop. There is probably a cleaner way to do this but I'll let you have fun with that in your script.
Also anywhere you see 'sendl' or some variation there of. This is a call to my logging method, I just used one that I found through a web search that does everything I need it to do.

# Adds Operating System One of ... condition to the App.
Function Add-GlobalConditionToApp
{
    [CmdletBinding(SupportsShouldProcess = $True)]
Param (
[Parameter(Mandatory = $true)]
[string]$ApplicationName,
[Parameter(Mandatory = $true)]
[string]$GlobalConditionName,
            [Parameter(Mandatory = $true)]
[string]$GlobalConditionOperator,
            [Parameter(Mandatory = $true)]
[string]$GlobalConditionValue
)

Process
{
            sendl("Checking Global Condition Rules to : "+$ApplicationName)
            $App = Get-WmiObject -ComputerName $SCCMServer -Namespace "root\sms\Site_$SCCMSiteCode" -Query "Select * From SMS_Application Where CI_ID = '$ApplicationName' And IsLatest = 'True'"
            $App.Get();

            $ApplicationName = $App.LocalizedDisplayName
            sendl("Application Name: "+$ApplicationName)

$AppObject = [Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer]::DeserializeFromString($App.SDMPackageXML)
# $App.SDMPackageXML
         
            ForEach ($DeploymentType in  $AppObject.DeploymentTypes) {
                       
        $ContainsOSRules = $False
        $DoPut = $False
        $AdditionalLog = $True                  
                   
            ForEach ($rule in $DeploymentType.Requirements.Expression)
            {
                $z = 0
                If ($rule.GetType().Name -eq "OperatingSystemExpression")
                {
                    sendl ("# > " + $ApplicationName +" > " + $DeploymentType.Title.ToString() + " > " + $App.CI_ID + " > " + $rule.Operands.RuleID)
                    If (($rule.Operands.RuleID -like "*server*") -or ($rule.Operands.RuleID -like "*android*")-or ($rule.Operands.RuleID -like "*ipad*")-or ($rule.Operands.RuleID -like "*iphone*")-or ($rule.Operands.RuleID -like "*mobile*")-or ($rule.Operands.RuleID -like "*handheld*")-or ($rule.Operands.RuleID -like "*windows*10*"))
                    {
                        sendl ($DeploymentType.Title.ToString())
                        sendl "Contains Non Desktop OS"
                    }
                    Else
                    {
                        sendl "Found existing"
                        sendl ($DeploymentType.Title.ToString())
                        $ContainsOSRules = $True
                        sendl "Removing Rule"
                   $DeploymentType.Requirements.RemoveAt($z)
                        $DoPut = $True
                    }
                }
                Else
                {
                    sendl ($DeploymentType.Title.ToString())
                    sendl "No OS Rule Found"
                    sendl2($ApplicationName +" > " + $DeploymentType.Title.ToString() + " > " + $App.CI_ID + " > " + "No OS Rule Found")
                    $AdditionalLog = $False
                }
                $z = $z+1
            }
            If ($ContainsOSRules -eq $True)
            {
            sendl "Creating new"
            $rule = New-SCCMGlobalConditionsRule -SCCMServer $SCCMServer -SCCMSiteCode $SCCMSiteCode -GlobalConditionName $GlobalConditionName -Operator $GlobalConditionOperator -Value $GlobalConditionValue

               if (!($DeploymentType.Requirements | Where Name -eq $rule.Name))
                        {
                       $DeploymentType.Requirements.Add($rule)
                            $DoPut = $True
                   }
         
            }
            If ($DoPut -eq $True)
                {
                    $App.SDMPackageXML = [Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer]::Serialize($AppObject)
                    #$App.SDMPackageXML
                    sendl "Attempting Put"
                    $App.Put()
                    sendl3($ApplicationName +" > " + $DeploymentType.Title.ToString() + " > " + $App.CI_ID + " > " + " Put Complete")
                }
            Else
                {
                    sendl "No Put Attempted"
                    #add output to another file for manual review
                    If ($AdditionalLog -eq $True)
                    {
                        sendl2($ApplicationName +" > " + $DeploymentType.Title.ToString() + " > " + $App.CI_ID + " > " + " No Put")
                    }
                }
            }
        }
}

Comments

  1. In your final function, you use this:

    $App = Get-WmiObject -ComputerName $SCCMServer -Namespace "root\sms\Site_$SCCMSiteCode" -Query "Select * From SMS_Application Where CI_ID = '$ApplicationName' And IsLatest = 'True'"

    but CI_ID is a number that corresponds to an application and youre comparing it to ApplicationName,which is a string, so that query fails. Did you do something to pull the CI_ID for the Application Name that got left out of your code?

    ReplyDelete
    Replies
    1. So that was actually some sloppy variable use on my part. The original way I had this coded was using the app name and then I found an issue where you could have multiple of the same app name. I have a portion in my final code that I didn't include on here as this was more of a basis code for you to modify as seen fit. But I think something along this is what you are looking for. I actually pass in the CI_ID into this method in the parameters. Below is an example for a single app based on name (assuming the app name is unique) and a call the the method you referenced above. Hope this helps.

      $AppName = Get-WmiObject -ComputerName $SCCMServer -Namespace "root\sms\Site_$SCCMSiteCode" -Query "Select CI_ID From SMS_Application WHERE LocalizedDisplayName = '$AppName' AND IsLatest = 'True' AND IsExpired = 'False'"
      Add-GlobalConditionToApp -ApplicationName $AppName.CI_ID -GlobalConditionName "Operating System" -GlobalConditionOperator "OneOf" -GlobalConditionValue "Windows/All_x64_Windows_7_Client;Windows/All_x86_Windows_8.1_Client;Windows/All_x64_Windows_8.1_Client;Windows/All_x64_Windows_10_and_higher_Clients;Windows/All_x86_Windows_10_and_higher_Clients"

      Delete
    2. I ended up doing something similar to get through that but thanks for the reply! I'm still having issues getting the whole thing to work unfortunately. There so much going on that I'm trying to follow and while I don't get any errors, nothing actually happen with my test app either.

      Delete
    3. Does your test app already have an OS requirement rule on it? I think my code only updates if it already has a rule. Its been a bit since I've used it. If you need to add a rule to one that doesn't already have one then you would have to modify that section.

      Delete
  2. Ah that might be why. No it does not already have one. I need to add one to a lot of apps which is why I'm trying to script it. If I have to add one in advance then it defeats the purpose of what I'm trying to do. Is there a way around it having to be there already? I found someone elses code in my research that also said that the requirement had to be present so I passed on that one.

    ReplyDelete
    Replies
    1. You have to change the Add-GlobalConditionToApp function so that it runs if no rule is found.
      Basically inside the "ForEach ($rule in $DeploymentType.Requirements.Expression)" change the if statement so that it runs always or reverse the if/else so that it only runs when no rule is found. Or just replace

      If ($rule.GetType().Name -eq "OperatingSystemExpression")
      {
      sendl ("# > " + $ApplicationName +" > " + $DeploymentType.Title.ToString() + " > " + $App.CI_ID + " > " + $rule.Operands.RuleID)
      If (($rule.Operands.RuleID -like "*server*") -or ($rule.Operands.RuleID -like "*android*")-or ($rule.Operands.RuleID -like "*ipad*")-or ($rule.Operands.RuleID -like "*iphone*")-or ($rule.Operands.RuleID -like "*mobile*")-or ($rule.Operands.RuleID -like "*handheld*")-or ($rule.Operands.RuleID -like "*windows*10*"))
      {
      sendl ($DeploymentType.Title.ToString())
      sendl "Contains Non Desktop OS"
      }
      Else
      {
      sendl "Found existing"
      sendl ($DeploymentType.Title.ToString())
      $ContainsOSRules = $True
      sendl "Removing Rule"
      $DeploymentType.Requirements.RemoveAt($z)
      $DoPut = $True
      }
      }
      Else
      {
      sendl ($DeploymentType.Title.ToString())
      sendl "No OS Rule Found"
      sendl2($ApplicationName +" > " + $DeploymentType.Title.ToString() + " > " + $App.CI_ID + " > " + "No OS Rule Found")
      $AdditionalLog = $False
      }

      With


      sendl ("# > " + $ApplicationName +" > " + $DeploymentType.Title.ToString() + " > " + $App.CI_ID + " > " + $rule.Operands.RuleID)
      If (($rule.Operands.RuleID -like "*server*") -or ($rule.Operands.RuleID -like "*android*")-or ($rule.Operands.RuleID -like "*ipad*")-or ($rule.Operands.RuleID -like "*iphone*")-or ($rule.Operands.RuleID -like "*mobile*")-or ($rule.Operands.RuleID -like "*handheld*")-or ($rule.Operands.RuleID -like "*windows*10*"))
      {
      sendl ($DeploymentType.Title.ToString())
      sendl "Contains Non Desktop OS"
      }
      Else
      {
      sendl "Found existing"
      sendl ($DeploymentType.Title.ToString())
      $ContainsOSRules = $True
      sendl "Removing Rule"
      $DeploymentType.Requirements.RemoveAt($z)
      $DoPut = $True
      }

      Delete
  3. Thanks for the help! Ill keep playing around with it. I cant get the original to work even after adding a requirement to my test app, yet I get no errors. I must be doing something wrong but Ill keep at it.

    ReplyDelete
    Replies
    1. You're welcome. Make sure the account you are using has sufficient rights and take a look at your smsprov.log as well for insights. You might be getting an error and not realize it. Also make sure you refresh your console after you run it to check. That one threw me for a bit. The app deployment type didn't update after the script ran and found I had to click onto another application and then click back to it for the console to pickup the changes.

      Delete

Post a Comment

Popular posts from this blog

Intune Hybrid - NDES Cert Issue

Stuck @ "Waiting for user logon"

Triggering a software update install via Powershell