14 November 2019

Posting to Multiple Facebook Groups

I publish new blog postings to multiple Facebook groups regularly. It can be daunting if there are a lot of groups. Although there are several online options, there is a Google Chrome extension called Toolkit for Facebook that is fabulous. It lets you post to multiple groups while being able to designate a random delay in between posting so you don't go to Facebook jail. 

Recently, the extension was pulled from the Chrome Store and is now only available on the website. I cannot get Chrome to accept the extension from the site, so I ended up going to an old machine of mine and copying the extension directory and importing it into my new Chrome browser. It works great. 

As shown below, I enabled developer mode, clicked load unpacked, and then selected the directory where the zip file was exploded to that contains the extension. I did have to close out the browser and reopen it afterward for the extension to appear. The extension can be downloaded from my box account located here



05 November 2019

SCCM Pending Reboot Report

We wanted a list of servers that are waiting for a reboot. Thankfully, SCCM has a pending restart field that allows admins to see when systems are waiting for a reboot. Our server team wanted that list to be automatically generated and emailed to them daily. Other than myself, others that have the SCCM console rarely look at it since they wear many hats in IT.

I could not find any PowerShell cmdlets in the SCCM module for viewing a pending restart. Thankfully, Eswar Koneti's blog shows the information is stored in sms_combineddeviceresources.

After learning that part, I decided it would be dramatically faster to query the SQL database directly. The table the information resides in is dbo.vSMS_CombinedDeviceResources and is under ClientState. ClientState will have a value of zero if no pending reboot, and a value between 2 and 15 if there is a pending reboot. The list of those values is in the above blog link.

In the PowerShell script below, there are three parameters you will need to populate. The first is $Collection that contains the name of the collection you want to query. $SQLServer is the name of the SQL server. Finally, $SQLDatabase is the name of the SCCM SQL database. You can populate them either at the command line, or hard code the data in the parameter field of the script.

I wrote the script in a way that it can be easily implemented into Azure Automation, SMA, or Orchestrator. The script will output the list of machines waiting for a reboot using the Write-Output and Exit with a return code of 1 if there is nothing to report. The exit code 1 is used with Orchestrator or SMA for the Link between the PowerShell script and the email. The link would not continue to the email task if there were an exit code of 1, as shown below.

NOTE: For this to access the SQL server, the script must be executed on the SQL server, or on a machine that has the SQL console. This is required, so PowerShell has access to the SQL PowerShell module.


You can download this script from my GitHub site.


 <#  
      .SYNOPSIS  
           SCCM Reboot Report  
        
      .DESCRIPTION  
           This script will query SCCM for a list of machines that are waiting for a reboot.  
        
      .PARAMETER Collection  
           Name of the collection to query  
        
      .PARAMETER SQLServer  
           Name of the SQL server  
        
      .PARAMETER SQLDatabase  
           A description of the SQLDatabase parameter.  
        
      .PARAMETER SQLInstance  
           Name of the SQL Database  
        
      .PARAMETER SCCMFQDN  
           Fully Qualified Domain Name of the SCCM server  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       10/3/2019 12:04 PM  
           Created by:       Mick Pletcher  
           Filename:         SCCMRebootReport.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()]  
      [string]$Collection,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLServer,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLDatabase  
 )  
   
 $RebootList = (Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query 'SELECT * FROM dbo.vSMS_CombinedDeviceResources WHERE ClientState <> 0').Name | Sort-Object  
 $CollectionQuery = 'SELECT * FROM' + [char]32 + 'dbo.' + ((Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query ('SELECT ResultTableName FROM dbo.v_Collections WHERE CollectionName = ' + [char]39 + $Collection + [char]39)).ResultTableName)  
 $CollectionList = (Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query $CollectionQuery).Name | Sort-Object  
 $List = @()  
 $RebootList | ForEach-Object { If ($_ -in $CollectionList) { $List += $_ } }  
 If ($List -ne '') {  
      Write-Output $List  
 } else {  
      Exit 1  
 }  
   

30 October 2019

End User Reboot Management

I have wrestled with the issue of managing mandatory reboots for quite a while. Back before laptops were introduced into the environment, we used an SCCM package that triggered a reboot once a week. Today, with the majority of systems being laptops and tablets, it is not so easy to implement a reboot. The problem came when the package ran, and a system was offline. It would not get done. The second issue comes from laptops being closed instead of shutdown. A reboot rarely happens.

I first started out by creating a complex SCCM application that used a custom detection method. It was not the most successful. After that, I have worked on and off for months until I finally figured out how to implement this. The first thing that needs to be done is to create an SCCM package that reboots the machine. The package is only advertised to it and not mandatory. The package program executes a PowerShell one-liner that exits with a return code of 3010. The after running is set to Configuration Manager restarts computers as shown below.




The PowerShell one-liner is as follows:

 powershell.exe -executionpolicy bypass -command "&{Exit 3010}"  

Under User Experience, both software installation and system restart are checked. Since this is an advertisement, allow users to run the program is checked by default.



Under Distribution Points, I set both to Run program from distribution point, so there is no need to download anything to the machine as it is a PowerShell one-liner that is executed.



We came up with the following list of requirements needed for the script. The requirements are:

  • The system has not been rebooted in X number of days. X is determined by the company and is implemented by populating the parameter $MaxDays with an integer value.
  • The ClientState does not equal zero. ClientState contains integer values from zero to fifteen. Zero means no pending reboots. Any other value from one to fifteen means the system is pending a reboot. Thanks to Eswar Koneti for that info! 
  • LastBootUpTime0 is not a null value. If there is a null value, it means the system has been offline for quite a while. 

The PowerShell script queries the SCCM SQL database for a list of systems. The above requirements were all implemented within the SQL query that PowerShell executes. Once the query is complete, PowerShell will go through the query results to generate an object for each machine. The object includes the computer name, the last boot-up time, if the system is reboot pending, and if the system is online. It then runs the function that will trigger the reboot.

In the function, I have it do a more thorough check if the system is online. It then reaches out to the machine with a WMI query to verify the system has not been rebooted in X amount of days. This is in case a hardware inventory has not been executed to update the SCCM SQL database, so a reboot is not run twice on a machine. It then retrieves the information of the above advertisement from SCCM. The script will change the advertisement from advertise to mandatory for that specific machine. This is necessary for the script to initiate the reboot package. It then executes the package and returns the package from mandatory to advertised.

The object containing the system information is then added to an array for reporting purposes. The report is printed to the screen at the end. If no machines were needing a reboot, then the script exits with an exit code of 1. The reason for this is in case the script is implemented in SMA/Orchestrator/Azure Automation, the Link can be specified not to continue on to send out an email if an error code of 1 was returned. If there were machines in a list, then the Write-Output makes that output list available to the next activity, such as email. I have ours set up to send an email out to the help desk with a list of machines that got the reboot implemented on in case users call in asking why.

As far as using the script, you will need to populate the parameter fields. A detailed description is in the script of each parameter.

This can be implemented in several ways. I implemented it using Orchestrator in a runbook. It can also be implemented by a manual execution of the PowerShell script. You could also set it up to run from a scheduled task. I have it execute every morning at 9 am.

The last thing that needs to be implemented is the computer restart settings. I created a collection named Reboot Compliance that I deployed the client settings and the package to. That way when I added machines to the collection, they got both the settings and package. These are the settings I use. This gives a user 8 hours to reboot with the final hour in which the reboot message will not go away at the bottom left of the screen.



NOTE: This script needs to be executed on a system that has the sqlserver PowerShell module.

NOTE: You will probably want to execute this script in a test mode. I would highly recommend commenting out lines 79 through 106 for this. The script will then only obtain a list of machines and not trigger reboots. 

You can download the script from my GitHub site.

 <#  
      .SYNOPSIS  
           LastBootup  
        
      .DESCRIPTION  
           This script will query the SCCM SQL database for a list of machines that have not been rebooted for more than ten days  
        
      .PARAMETER Collection  
           Name of the collection to query. PowerShell will find the SQL table name from the collection name
        
      .PARAMETER SQLServer  
           Name of the SQL server that contains the SCCM database
        
      .PARAMETER SQLDatabase  
           Name of the SCCM SQL database  
        
      .PARAMETER DeploymentName  
           Name of deployment of the SCCM reboot package
        
      .PARAMETER MaxDays  
           If system has not rebooted for this number of days, then add to $Report  
        
      .PARAMETER SQLInstance  
           Name of the SQL Database  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       10/22/2019 10:21 AM  
           Created by:       Mick Pletcher  
           Filename:         RebootManagement.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()]  
      [string]$Collection,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLServer,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLDatabase,  
      [ValidateNotNullOrEmpty()]  
      [string]$DeploymentName,  
      [ValidateNotNullOrEmpty()]  
      [int]$MaxDays  
 )  
   
 function Initialize-Reboot {  
 <#  
      .SYNOPSIS  
           Initialize Timed Reboot  
        
      .DESCRIPTION  
           This will change the reboot package from advertised to mandatory in order to initiate the reboot. It will then change package back to advertised once the package has been executed.  
        
      .PARAMETER Object  
           A description of the Object parameter.  
        
      .EXAMPLE  
           PS C:\> Initialize-Reboot  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           $Object  
      )  
        
      If ((Test-Connection -ComputerName $Object.Name -Quiet) -eq $true) {  
           #Query the remote system to make sure it has not been rebooted since the LastBootUpTime was updated to SCCM  
           If ((New-TimeSpan -Start ([Management.ManagementDateTimeConverter]::ToDateTime((Get-WmiObject -Class win32_operatingsystem -ComputerName $Object.Name).LastBootUpTime))).Days -gt $MaxDays) {  
                #Package and Advertisement IDs of the SCCM package  
                $Advertisement = Get-WmiObject -Namespace "root\ccm\policy\machine\actualconfig" -Class "CCM_SoftwareDistribution" -ComputerName $Object.Name | Where-Object {$_.PKG_Name -eq $DeploymentName} | Select-Object -Property PKG_PackageID, ADV_AdvertisementID  
                #Continue to next array item if the advertisement is null, meaning the reboot application was not advertised to the system  
                If ($Advertisement -ne $null) {  
                     #Schedule IS of the SCCM package deployment  
                     $ScheduleID = Get-WmiObject -Namespace "root\ccm\scheduler" -Class "CCM_Scheduler_History" -ComputerName $Object.Name | Where-Object {  
                          $_.ScheduleID -like "*$($Advertisement.PKG_PackageID)*"  
                     } | Select-Object -ExpandProperty ScheduleID  
                     #Retrieve advertisement policy  
                     $Policy = Get-WmiObject -Namespace "root\ccm\policy\machine\actualconfig" -Class "CCM_SoftwareDistribution" -ComputerName $Object.Name | Where-Object {  
                          $_.PKG_Name -eq $DeploymentName  
                     }  
                     #Change advertisement policy to mandatory so the package can be executed  
                     If ($Policy.ADV_MandatoryAssignments -eq $false) {  
                          $Policy.ADV_MandatoryAssignments = $true  
                          $Policy.Put() | Out-Null  
                     }  
                     #Execute the advertisement  
                     Invoke-WmiMethod -Namespace "root\ccm" -Class "SMS_Client" -Name "TriggerSchedule" -ArgumentList $ScheduleID -ComputerName $Object.Name  
                     #Wait one second to give time for the package to initiate  
                     Start-Sleep -Seconds 1  
                     #Retrieve advertisement policy  
                     $Policy = Get-WmiObject -Namespace "root\ccm\policy\machine\actualconfig" -Class "CCM_SoftwareDistribution" -ComputerName $Object.Name | Where-Object {  
                          $_.PKG_Name -eq $DeploymentName  
                     }  
                     #Remove the mandatory assignment from the package so this is not rerun  
                     If ($Policy.ADV_MandatoryAssignments -eq $true) {  
                          $Policy.ADV_MandatoryAssignments = $false  
                          $Policy.Put() | Out-Null  
                     }  
                }  
           }  
      } else {  
           Return $null  
      }  
      Return $object  
 }  
   
 #Get the table name from the $Collection value  
 $TableName = 'dbo.' + ((Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query ('SELECT ResultTableName FROM dbo.v_Collections WHERE CollectionName = ' + [char]39 + $Collection + [char]39)).ResultTableName)  
 #Query active systems in the above table to list the computer name and the last boot up time  
 $Query = 'SELECT Name, LastBootUpTime0, ClientState FROM dbo.v_GS_OPERATING_SYSTEM INNER JOIN' + [char]32 + $TableName + [char]32 + 'ON dbo.v_GS_OPERATING_SYSTEM.ResourceID =' + [char]32 + $TableName + '.MachineID WHERE ((((DATEDIFF(DAY,LastBootUpTime0,GETDATE())) >' + [char]32 + $MaxDays + ') OR ClientState <> 0) AND LastBootUpTime0 IS NOT NULL)'  
 #Query SCCM SQL database  
 $List = Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query $Query  
 #Create report array to contain list of all systems that have not rebooted for $Threshold days  
 $Report = @()  
 #Check if list is null. If so, exit with error code 1 so if script is used in Orchestrator or SMA, it can be set to not proceed with email.  
 If ($List -ne '') {  
      #Get list of machines that are exceed number of days allowed without rebooting. This is also used as a report if desired to be emailed.  
      $List | ForEach-Object {  
           If ($_.ClientState -ne 0) {  
                $PendingReboot = $true  
           } else {  
                $PendingReboot = $false  
           }  
           If ((Test-Connection -ComputerName $_.Name -Count 1 -Quiet) -eq $true) {  
                $Online = $true  
           } else {  
                $Online = $false  
           }  
           #Create new object  
           $object = New-Object -TypeName System.Management.Automation.PSObject  
           $object | Add-Member -MemberType NoteProperty -Name Name -Value $_.Name  
           $object | Add-Member -MemberType NoteProperty -Name LastBootUpTime -Value $_.LastBootUpTime0  
           $object | Add-Member -MemberType NoteProperty -Name PendingReboot -Value $PendingReboot  
           $object | Add-Member -MemberType NoteProperty -Name Online -Value $Online  
           If ($object.Online -eq $true) {  
                $obj = Initialize-Reboot -Object $object  
           }  
           #If the reboot was initiated, then add to $Report  
           If ($obj -ne $null) {  
                $Report += $obj  
           }  
      }  
      If ($Report -eq $null) {  
           #This exit code is used for signaling to a link in Orchestrator or Azure Automation to not proceed to the next activity  
           Write-Host "Null"  
           Exit 1  
      } else {  
           Write-Output $Report | Sort-Object LastBootUpTime, Name  
      }  
 } else {  
      #This exit code is used for signaling to a link in Orchestrator or Azure Automation to not proceed to the next activity  
      Write-Host "Null"  
      Exit 1  
 }  
   

15 October 2019

SCCM Duplicate Machine Cleanup

I got tired of duplicate systems appearing in SCCM caused by computers being reimaged while using the same computer name. To rid myself of this issue, I wrote the script below.

It queries the SCCM SQL database for a list of machines where the SCCM client installation was attempted with a return code of 120. This error code indicates the system is already present and active in SCCM, thereby indicating this system is the old one.

The script was designed to work with Orchestrator, SMA, Azure Automation, a scheduled task, or manual execution. Exit 1 is there if it is used with Orchestrator. Commented out line 68 is there for you to use and test this script. I highly recommend uncommenting that line and then commenting out line 69 to verify first before the script actually deletes systems from SCCM. 

You can download this script from my GitHub Repository.


 <#  
      .SYNOPSIS  
           SCCM Duplicate Cleanup  
        
      .DESCRIPTION  
           This script will query for a list of machines with error 120 when trying to install the SCCM client. This error indicates the system is a duplicate.   
        
      .PARAMETER SCCMModule  
           UNC path including file name of the configuration manager module  
        
      .PARAMETER SCCMServer  
           FQDN of SCCM Server  
        
      .PARAMETER SCCMSiteDescription  
           Description of the SCCM Server  
        
      .PARAMETER SiteCode  
           Three letter SCCM Site Code  
        
      .PARAMETER Collection  
           Name of the collection to query  
        
      .PARAMETER SQLServer  
           Name of the SQL server  
        
      .PARAMETER SQLDatabase  
           A description of the SQLDatabase parameter  
        
      .PARAMETER SQLInstance  
           Name of the SQL Database  
        
      .PARAMETER SCCMFQDN  
           Fully Qualified Domain Name of the SCCM server  
        
      .NOTES  
           ===========================================================================  
           Created with:      SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       10/3/2019 12:04 PM  
           Created by:       Mick Pletcher  
           Filename:          SCCMDuplicateCleanup.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()]  
      [string]$SCCMModule,  
      [ValidateNotNullOrEmpty()]  
      [string]$SCCMServer,  
      [ValidateNotNullOrEmpty()]  
      [string]$SCCMSiteDescription,  
      [ValidateNotNullOrEmpty()]  
      [string]$SiteCode,  
      [ValidateNotNullOrEmpty()]  
      [string]$Collection,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLServer,  
      [ValidateNotNullOrEmpty()]  
      [string]$SQLDatabase  
 )  
   
 $List = Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query ('SELECT Name, MachineID, CP_LastInstallationError FROM' + [char]32 + 'dbo.' + ((Invoke-Sqlcmd -ServerInstance $SQLServer -Database $SQLDatabase -Query ('Select ResultTableName FROM dbo.Collections WHERE CollectionName =' + [char]32 + [char]39 + $Collection + [char]39)).ResultTableName) + [char]32 + 'WHERE ClientVersion IS NULL AND CP_LastInstallationError = 120 Order By MachineID')  
 If ($List -ne '') {  
      Import-Module -Name $SCCMModule -Force  
      New-PSDrive -Name $SiteCode -PSProvider 'AdminUI.PS.Provider\CMSite' -Root $SCCMServer -Description $SCCMSiteDescription | Out-Null  
      Set-Location -Path ($SiteCode + ':')  
      #Test with output to screen before enabling the other line that also deletes each item  
      $List | ForEach-Object { (Get-CMDevice -ResourceId $_.MachineID -Fast).Name }  
      #$List | ForEach-Object { Get-CMDevice -ResourceId $_.MachineID -Fast | Remove-CMDevice -Confirm:$false -Force }  
      Remove-PSDrive -Name $SiteCode -Force  
      Write-Output ($List.Name | Sort-Object)  
 } else {  
      Exit 1  
 }  
   

03 October 2019

Importing and Using the SCCM PowerShell Module

Recently, I have begun setting up new front and back-office security runbooks in Microsoft Orchestrator. These runbooks needed to use PowerShell for getting data from the SCCM server. The SCCM console is not installed on the Orchestrator server, so PowerShell required to be able to connect directly to it.

The following script will import the SCCM PowerShell module, connect to the SCCM server, and then disconnect. I used the cmdlet Get-CMCollectionMember for all systems because that is shared in all SCCMs.

The first thing that needs to be done is to define the UNC path to the SCCM module. The module is located at Program Files\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1. I would put the drive letter in front, but like myself, you may not have SCCM installed on the c: drive. The next thing that needs to be defined is the SCCM server name. I used the FQDN for that. The SCCM Site Description is needed to map the drive. The description was made up of calling it Primary SCCM Site. Finally, the three-letter site code is necessary. That is the directory you will need to change to inside PowerShell before you can call any cmdlets.

Once these items are defined, you will need to first import the SCCM module. Next, you will need to open the connection to it through using the New-PSDrive. The directory then needs to be changed to the defined PSDrive. Now the SCCM cmdlets can be utilized. I included removing the PSDrive at the end of the script.

You can download the script from my GitHub site.


 <#  
      .SYNOPSIS  
           PowerShell SCCM Connection  
        
      .DESCRIPTION  
           This script will connect to the SCCM server and return a list of all systems in SCCM. It is a demo on how to accomplish this task.  
        
      .PARAMETER SCCMModule  
           UNC path including file name of the configuration manager module  
        
      .PARAMETER SCCMServer  
           A description of the SCCMServer parameter.  
        
      .PARAMETER SCCMSiteDescription  
           Description of the SCCM Server  
        
      .PARAMETER SiteCode  
           Three letter SCCM Site Code  
        
      .PARAMETER SCCMFQDN  
           Fully Qualified Domain Name of the SCCM server  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       4/24/2019 9:56 AM  
           Created by:       Mick Pletcher  
           Filename:         SCCMCleanup.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()]  
      [string]$SCCMModule = '<UNCPath>\ConfigurationManager.psd1',  
      [ValidateNotNullOrEmpty()]  
      [string]$SCCMServer = '<FQDN of SCCM Server>',  
      [ValidateNotNullOrEmpty()]  
      [string]$SCCMSiteDescription = '<SCCM Server Description>',  
      [ValidateNotNullOrEmpty()]  
      [string]$SiteCode = '<Three letter SCCM Site Code>'  
 )  
   
 Import-Module -Name $SCCMModule -Force  
 New-PSDrive -Name $SiteCode -PSProvider 'AdminUI.PS.Provider\CMSite' -Root $SCCMServer -Description $SCCMSiteDescription | Out-Null  
 Set-Location -Path ($SiteCode + ':')  
 $List = Get-CMCollectionMember -CollectionName 'All Systems'  
 Remove-PSDrive -Name $SiteCode -Force  
 Write-Output $List.Name  
   



11 September 2019

Zero Touch Dell Command Update for SCCM and MDT

I have used the Dell Command | Update in the build for quite some time for managing the drivers on systems because it makes it a hand-off approach with little setup and reliable updates direct from Dell. The one thing I have wanted to be able to rerun this task several times without having to have duplicate tasks in the task sequence. Sometimes there are multiple reboots required because not all drivers can be installed at the same time.

I finally discovered how ZTIWindowsUpdate.wsf reboots and reruns itself without corrupting the task sequence and I have applied the same code to this script. 

This script will run the dcu-cli.exe to both install updates and generate inventory.xml and activitylog.xml files. The script then reads the activitylog.xml file to see if any drivers were installed. If not, the script ends. If there were updates installed, the script creates a rebootcount.log file if it does not exist, that contains the number of reboots that have been performed. It will increment that number inside the file each time the system is rebooted and the script is rerun. This had to be included because a few updates Dell deploys do not register with the Dell Command | Update and reinstall every time it is run. Because of that, I limited the script to run 5 times like the ZTIWindowsUdpates.wsf does. I wrote in another blog entry why a task sequence variable cannot be used here to store the reboot count. 

Once the script either has no more updates to install or has run 5 times, it will remove both xml files and end so that SCCM/MDT can continue to the next task. 

The script can be downloaded from my GitHub site


 <#  
      .SYNOPSIS  
           Dell Driver Update  
        
      .DESCRIPTION  
           This script executes the Dell Command | Update and reboots MDT or SCCM up to five times when new updates are available. The reason for the 5 reboot limit is because some Dell updates have been know to not leave markers and will cause the Dell Command | Update to rerun indefinitely.  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       7/18/2019 1:57 PM  
           Created by:       Mick Pletcher  
           Filename:         ZTIDellDriverUpdate.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param ()  
 #Delete old logs if they exist  
 Remove-Item -Path ($env:windir + '\temp\ActivityLog.xml') -ErrorAction SilentlyContinue -Force  
 Remove-Item -Path ($env:windir + '\temp\inventory.xml') -ErrorAction SilentlyContinue -Force  
 #Update the system with the latest drivers while also writing log files to the %windir%\temp directory  
 $ErrCode = (Start-Process -FilePath ((Get-ChildItem -Path $env:ProgramFiles, ${env:ProgramFiles(x86)} -Filter 'dcu-cli.exe' -Recurse).FullName) -ArgumentList ('/log ' + $env:windir + '\temp') -Wait).ExitCode  
 #Read the ActivityLog.xml file  
 $File = (Get-Content -Path ($env:windir + '\temp\ActivityLog.xml')).Trim()  
 #if no updates were found or updates were applied and no required reboot is necessary, then delete the log files  
 If (('<message>CLI: No application component updates found.</message>' -in $File) -and (('<message>CLI: No available updates can be installed.</message>' -in $File) -or ('<message>CLI: No updates are available.</message>' -in $File))) {  
      Remove-Item -Path ($env:windir + '\temp\ActivityLog.xml') -ErrorAction SilentlyContinue -Force  
      Remove-Item -Path ($env:windir + '\temp\inventory.xml') -ErrorAction SilentlyContinue -Force  
      Remove-Item -Path ($env:TEMP + '\RebootCount.log') -ErrorAction SilentlyContinue -Force  
 } else {  
      #Create the file containing number of times this script has rerun if it does not exist  
      If ((Test-Path ($env:TEMP + '\RebootCount.log')) -eq $false) {  
           New-Item -Path ($env:TEMP + '\RebootCount.log') -ItemType File -Value 0 -Force  
      }  
      #Reboot the machine and rerun the Dell Driver Updates  
      If (([int](Get-Content -Path ($env:TEMP + '\RebootCount.log'))) -lt 5) {  
           #Microsoft SCCM/MDT environmental variables  
           $TSEnv = New-Object -ComObject Microsoft.SMS.TSEnvironment  
           #Reboot the machine once this task is completed and restart the task sequence  
           $TSEnv.Value('SMSTSRebootRequested') = $true  
           #Rerun the same task  
           $TSEnv.Value('SMSTSRetryRequested') = $true  
           #increment the reboot counter  
           New-Item -Path ($env:TEMP + '\RebootCount.log') -ItemType File -Value ([int](Get-Content -Path ($env:TEMP + '\RebootCount.log')) + 1) -Force  
      #End the update process if run 5 or more times, delete all associated log files, and proceed to the next task  
      } else {  
           Remove-Item -Path ($env:windir + '\temp\ActivityLog.xml') -ErrorAction SilentlyContinue -Force  
           Remove-Item -Path ($env:windir + '\temp\inventory.xml') -ErrorAction SilentlyContinue -Force  
           Remove-Item -Path ($env:TEMP + '\RebootCount.log') -ErrorAction SilentlyContinue -Force  
      }  
 }  
   

05 September 2019

Custom Task Sequence Variables and the SMSTSRebootRequested

While writing a new PowerShell script for SCCM, I needed to be able to keep track of how many times a system had rebooted when using the SMSTSRebootRequested. The apparent solution, at least what I initially thought, was to create a TS variable and increment it upon each reboot.

It ended up never ceasing the reboot process, and the task reran indefinitely. I began debugging the variables by using message boxes to display the value of the associated TS variable. I found the newly created TS variable was disappearing every time the system rebooted. I proceeded to comment out the SMSTSRebootRequested line and made a copy of the task so that it would not rerun itself, but would continue to run the very next task, which was the exact same thing. Sure enough, upon executing the second task, the values did increment and had not been cleared out.

The solution to this was to write the values assigned to the custom task sequence variable to a text file on the drive that would be changed each time the task sequence reran. At the last execution, the file is deleted. 

30 August 2019

SCCM/MDT Windows Updates Installer

One of the issues I have had when moving the build process from MDT to SCCM has been windows updates. Windows updates are not available in the SCCM build if it does not already deploy the updates to machines. Except for servers, the user machines are configured to install updates pushed down by Microsoft as shown below. Before asking about managing the updates, I do have tools put in place to be able to quickly stop updates from installing and remove updates if issues come up.


To get updates installed in the SCCM build process, I started by downloading and installing the PSWindowsUpdate PowerShell Module written by Michal Gajda. This module is fantastic for managing updates.

Next, I wrote the following PowerShell script that does the following things. The first thing that needs to be done is to make sure the PowerShellGet module is present before running the script. The script will check if the latest version of PSWindowsUpdate is installed. If not, it installs it. This will also install it if it is not already present. Next, it will query Microsoft for a list of missing updates. If there are no updates, then the script exits with an error code 0. If there are updates, it will proceed to install them. There was one problem I ran into when writing this script. It did not have sufficient privileges to install the updates. It needed to be run-as-administrator. The workaround for this is to add the account the script is run under to the administrators group on the local machine, which is what the script does. After adding to the administrators group, it installs the updates, while suppressing a reboot. After this is finished, it removes the user from the administrators group. There are four conditions it will then check for to see if a reboot is pending. If it is pending, then the script will initiate a reboot via SCCM if the -SCCM switch is in the parameters field, and it instructs SCCM to rerun the same task. If there is not a pending reboot, then the script checks for additional updates and installs them.

To implement it in SCCM, I used a Run PowerShell Script task sequence. The entire script below was pasted into the Edit Script field of the Enter a PowerShell Script:. I did also use the -SCCM parameter. he Run this step as the following account was also selected that uses the build account.



You can download the script from my GitHub site.


 <#  
      .SYNOPSIS  
           Install Windows Updates  
        
      .DESCRIPTION  
           This script uses the PSWindowsUpdate module to install the latest windows updates. It also makes sure the latest version of the module is installed. It is designed to run in SCCM, MDT, and in a deployment.  
        
      .PARAMETER SCCM  
           This specifies for the script to use the Microsoft.SMS.TSEnvironment comobject for managing the reboot and re-execution of the Windows Update Task  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       8/29/2019 7:22 AM  
           Created by:       Mick Pletcher  
           Filename:         InstallWindowsUpdates.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [switch]$SCCM  
 )  
 function Enable-Reboot {  
 <#  
      .SYNOPSIS  
           Reboot Machine  
        
      .DESCRIPTION  
           This function will reboot the machine. If the SCCM switch is defined, it will use the task sequence environmental variables to reboot the machine and restart the task sequence.   
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      If ($SCCM.IsPresent) {  
           $TaskSequence = New-Object -ComObject Microsoft.SMS.TSEnvironment  
           #Rerun this task when the reboot finishes   
           $TaskSequence.Value('SMSTSRetryRequested') = $true  
           #Reboot the machine when this command line task sequence finishes   
           $TaskSequence.Value('SMSTSRebootRequested') = $true  
      } else {  
           Restart-Computer -Force  
      }  
 }  
   
 Import-Module PowerShellGet  
 Import-Module -Name PSWindowsUpdate -ErrorAction SilentlyContinue  
 #Get the version of PSWindowsUpdate that is currently installed  
 $InstalledVersion = (Get-InstalledModule -Name PSWindowsUpdate).Version.ToString()  
 #Get the current version of PSWindowsUpdate that is available in the PSGallery  
 $PSGalleryVersion = (Find-Module -Name PSWindowsUpdate).Version.ToString()  
 #Uninstall and install PSWindowsUpdate module if the installed version does not match the version in PSGallery  
 If ($InstalledVersion -ne $PSGalleryVersion) {  
      Install-Module -Name PSWindowsUpdate -Force  
 }  
 #Get the list of available windows updates  
 $Updates = Get-WindowsUpdate  
 If ($Updates -ne $null) {  
      $NewUpdates = $true  
      Do {  
           #Add $AdminUser to Administrators group  
           Add-LocalGroupMember -Group Administrators -Member ($env:USERDOMAIN + '\' + $env:USERNAME)  
           #Install windows updates  
           Install-WindowsUpdate -AcceptAll -IgnoreReboot -Confirm:$false  
           #Remove $AdminUser from the Administrators group  
           Remove-LocalGroupMember -Group Administrators -Member ($env:USERDOMAIN + '\' + $env:USERNAME)  
           #Component Based Reboot   
           If ((Get-ChildItem "REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -ErrorAction SilentlyContinue) -ne $null) {  
                Enable-Reboot  
                $NewUpdates = $false  
           #Windows Update Reboot   
           } elseif ((Get-Item -Path "REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -ErrorAction SilentlyContinue) -ne $null) {  
                Enable-Reboot  
                $NewUpdates = $false  
           #Pending Files Rename Reboot   
           } elseif ((Get-ItemProperty -Path "REGISTRY::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -ErrorAction SilentlyContinue) -ne $null) {  
                Enable-Reboot  
                $NewUpdates = $false  
           #Pending SCCM Reboot   
           } elseif ((([wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities").DetermineIfRebootPending().RebootPending) -eq $true) {  
                Enable-Reboot  
                $NewUpdates = $false  
           }  
           #If no pending reboot, then check for new updates  
           If ($NewUpdates -eq $true) {  
                #Check for new windows updates  
                $Updates = Get-WindowsUpdate  
                #No reboot was required from last installed updates, so check if new updates are available and end loop if not  
                If ($Updates -eq $null) {  
                     $NewUpdates -eq $false  
                }  
           }  
      } While ($NewUpdates -eq $true)  
 } else {  
      Exit 0  
 }  
   

21 August 2019

Extract SCCM WIM from ISO and Configure for WDS via PowerShell

One issue, more like a time-waster, I have had as we are migrating to SCCM from MDT is getting the boot image on the WDS server. SCCM is not configured to manage WDS because we use it for other things, so we manually upload the WIM. SCCM creates an ISO that needs to be mounted to extract the WIM for WDS. Thanks to this blog post, it is explained. I wanted the steps automated, so I wrote the following PowerShell script that will perform all of the steps needed to extract and configure the WIM.

To use this, populate the full file name in the $File variable. The script will do the rest. It is documented throughout the script so that it is easy to follow. You can download the script from my GitHub site.


 <#  
      .SYNOPSIS  
           Extract and Configure SCCM WIM Boot Image  
        
      .DESCRIPTION  
           This script will extract the WIM file from the ISO boot image generated by SCCM. It then mounts, configures, and unmounts the WIM file so it is ready to push up to WDS.  
        
      .PARAMETER File  
           Filename of the ISO, which includes the full path.  
        
      .NOTES  
           ===========================================================================  
           Created with:    SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:      8/21/2019 12:59 PM  
           Created by:      Mick Pletcher  
           Filename:        SCCMBootImage.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()]  
      [string]$File = 'C:\WinPE\SCCM.iso'  
 )  
   
 Import-Module Dism  
 #Extract the name of the directory the $File resides in  
 $Directory = $File.substring(0, $File.LastIndexOf('\'))  
 #Mount and assign drive letter  
 $Drive = ((Mount-DiskImage -ImagePath $File) | Get-Volume).DriveLetter  
 #Delete old Mount directory  
 Remove-Item -Path ($Directory + '\Mount') -Recurse -Force -ErrorAction SilentlyContinue  
 #Create Mount folder to copy WIM contents to  
 New-Item -Path ($Directory + '\Mount') -ItemType Directory -Force  
 #Delete old WIM file  
 Remove-Item -Path ($Directory + '\boot.wim') -ErrorAction SilentlyContinue -Force  
 #Copy boot.WIM file to $Directory  
 Copy-Item -Path ($Drive + ':\sources\boot.wim') -Destination $Directory -Force  
 #Turn off read only  
 Set-ItemProperty -Path ($Directory + '\boot.wim') -Name IsReadOnly -Value $false  
 #Mount the WIM file  
 Mount-WindowsImage -ImagePath ($Directory + '\boot.wim') -Index 1 -Path ($Directory + '\Mount')  
 #Copy data folder to mounted image  
 Copy-Item -Path ($Drive + ':\SMS\data') -Destination ($Directory + '\Mount\sms') -Recurse -Force  
 #Unmount Windows Image  
 Dismount-WindowsImage -Path ($Directory + '\Mount') -Save  
 #Dismount disk image  
 Dismount-DiskImage -ImagePath $File  
 #Delete the old wim if it exists  
 Remove-Item -Path ((Get-ChildItem -Path ($Directory + '\' + ((Get-ChildItem -path $File -ErrorAction SilentlyContinue).Basename) + '.wim')).FullName) -Force -ErrorAction SilentlyContinue  
 #Rename boot.wim to match the basename of the ISO  
 Rename-Item -Path ($Directory + '\boot.wim') -NewName ((Get-ChildItem -Path $File).BaseName + '.wim') -Force  
 #Delete the Mount folder  
 Remove-Item -Path ($Directory + '\Mount') -Recurse -Force -ErrorAction SilentlyContinue  
   

20 August 2019

Batch file wait for process using PowerShell integration

Recently, I wanted a simple batch file to run the ccmsetup.exe file. I could have used PowerShell, but for debugging the parameters, I wanted it quick and easy to modify. One of the issues I had was getting the batch script to wait for the ccmexec.exe to install before proceeding. I saw some batch solutions that seemed to be kind of cumbersome. I decided to write a PowerShell one-liner that would continuously loop until the desired process ended. To do this, I used the following one-liner and pasted it into the batch file after the ccmsetup.exe was triggered. I wrote this so that all you have to do is change the assigned value to the $ProcessName. I included in the script the ability to remove the file extension in the event it is accidentally used. You can see below how I used it in the batch script:




 powershell.exe -executionpolicy bypass -command "&{$ProcessName='ccmsetup';Do {$Process=get-process ($ProcessName.split('.')[0]) -erroraction SilentlyContinue;Start-Sleep -seconds 2} While ($Process -ne $null)}"  

08 August 2019

Verifying SCCM package has updated the content on the distribution point

One of the things I have wrestled with over the years of using SCCM is verifying if the package on the distribution point contains the new content after updating the distribution point(s). I have typically seen that if there is a minor change, such as maybe a line of two of code changed in a PowerShell installer script, it doesn't get updated the first time around.

While working on the migration of the imaging process from MDT to SCCM, it occurred to me that there is a way to verify. I have typically run the update distribution points at least twice to make sure they all got updated. The easy way to verify is to set up a package share in the properties as shown below.


Now you can watch the package located at \\<SCCM Server Name>\<Share name> to see if the changes take place. They will change in real-time as soon as SCCM updates it. If you are in a testing phase and are having to update the distribution point(s) a lot like I am right now, this is a good way to test after each update.

NOTE: There still is not a way to do this with application deployments. It would be nice if the share option was added there! 

02 August 2019

MDT: How to initiate a reboot during a task without corrupting the task sequence

Recently, I have been working on updating several scripts I have written for the build process. One big thing I have wanted is for the script to be able to initiate a reboot without the build process becoming corrupt. An additional functionality I have wanted to implement is to be able to restart the task sequence at the same point it left off before the reboot.

I knew the task sequence reruns the windows update process multiple times, so I started by looking at the ZTIWindowsUpdate.wsf file. While combing through the file, I found the two items used for this process. They are:

  • SMSTSRebootRequested that reboots the machine
  • SMSTSRetryRequested tells the script to rerun the same task. 
I wanted this to be written in PowerShell, so next was figuring out how to access these two MDT environmental variables. After researching, I found that I can load the comobject Microsoft.SMS.TSEnvironment and that will give access to them. The following three lines of code are all that is needed in a PowerShell script to initiate a reboot and/or rerun the same task once it completes. Setting the above-listed variables to $true is what is required. If you just want to rerun the same task, you are not required to initiate the reboot. Once the task is completed, it will rerun again if SMSTSRetryRequested is specified at the end. 


      $TaskSequence = New-Object -ComObject Microsoft.SMS.TSEnvironment  
      #Reboot the machine this command line task sequence finishes  
      $TaskSequence.Value('SMSTSRebootRequested') = $true  
      #Rerun this task when the reboot finishes  
      $TaskSequence.Value('SMSTSRetryRequested') = $true  

18 July 2019

MDT Conditional Reboot

I wrote an article about three years ago on conditional task sequence reboots. It used the built-in reboot task sequence that was initiated only if any of the three conditions were met. The problem was a fourth condition that could not be tested for because a WMI query is the only way to test and MDT conditions do not incorporate WMI.

Recently, I revisited this, and it occurred to me how to incorporate the WMI query after going through the ZTIWindowsUpdate.wsf and seeing how it initiated reboots. I abandoned the built-in reboot and wrote a PowerShell script that can test all four conditions and then connect to the TSEnvironment object to start a reboot.

NOTE: The fourth test depends on the SCCM client already being installed.

The four conditions the script checks for are:

  • Component Based Servicing
  • Windows Updates
  • Pending Files Rename
  • Pending reboot from SCCM installs
The script will iterate through all four conditions. If a condition is met, it will then connect to the TSEnvironment object and request a reboot by setting SMSTSRebootRequested to true. Once the script is finished, the system will reboot and then proceed to the next task.

I also included the commented out SMSTSRetryRequested in the script. This command will cause the task sequence to rerun this script. I included it in here so if you want to take the code from this script and incorporate it into another script that will rerun it after the reboot, it is there. 

The first thing to do is to copy the script to the scripts (%SCRIPTROOT%) directory. As you can see in the screenshot below, I used a Run Command Line task sequence.



The command line is as follows:

 powershell.exe -executionpolicy bypass -file "%SCRIPTROOT%\ZTIConditionalReboot.ps1"  


Finally, here is the script. You can download it from my GitHub site.

 <#  
      .SYNOPSIS  
           Zero Touch Conditional Reboot  
        
      .DESCRIPTION  
           This script will check four flags on the system to see if a reboot is required. If one of the flags is tripped, then this script will initiate a reboot in MDT so that will come back up and start at the proceeding task. I have included the commented out SMSTSRetryRequested in the script so if you want to incorporate the code from this script into another one that will need to be rerun again once the reboot completes.   
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.142  
           Created on:       7/12/2019 2:53 PM  
           Created by:       Mick Pletcher  
           Organization:     Waller Lansden Dortch & Davis, LLP.  
           Filename:         ZTIConditionalReboot.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param ()  
   
 function Enable-Reboot {  
 <#  
      .SYNOPSIS  
           Request MDT Reboot  
        
      .DESCRIPTION  
           A detailed description of the Enable-Reboot function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      $TaskSequence = New-Object -ComObject Microsoft.SMS.TSEnvironment  
      #Reboot the machine this command line task sequence finishes  
      $TaskSequence.Value('SMSTSRebootRequested') = $true  
      #Rerun this task when the reboot finishes  
      #$TaskSequence.Value('SMSTSRetryRequested') = $true  
 }  
   
 #Component Based Reboot  
 If ((Get-ChildItem "REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -ErrorAction SilentlyContinue) -ne $null) {  
      Enable-Reboot  
 #Windows Update Reboot  
 } elseif ((Get-Item -Path "REGISTRY::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -ErrorAction SilentlyContinue) -ne $null) {  
      Enable-Reboot  
 #Pending Files Rename Reboot  
 } elseif ((Get-ItemProperty -Path "REGISTRY::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -ErrorAction SilentlyContinue) -ne $null) {  
      Enable-Reboot  
 #Pending SCCM Reboot  
 } elseif ((([wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities").DetermineIfRebootPending().RebootPending) -eq $true) {  
      Enable-Reboot  
 } else {  
      Exit 0  
 }  
   


11 June 2019

Fixing Do you want to run this file? during SCCM Deployment

Over the past two months, I deployed the Windows 10 1809 to all Windows 10 machines. We got through 80% of the machines with successful deployments until we reached those last 20% where they did not have enough disk space for both downloading the package and installing it. The package and install require roughly 15 gigs of space, 5 gigs for the package and 10 gigs for the installation.

We changed the distribution points option to run the program from the distribution point to drop down the 15 gig requirement to 10 gigs, which also significantly reduced the number of machines without enough space.


Once we made that change, systems started getting the message shown below. This caused the package to stall since there was no one to click run.



To get around this issue, we added the FQDN of each distribution point to the intranet trusted sites via GPO. The FQDN is added like shown here: \\<SCCMServer.contoso.com>\ and set as security zone 1. This fixed that issue. 

06 June 2019

Configuring Power Scheme with a PowerShell One-Liner

Recently, we decided to change the power scheme on machines during the build. This can be quickly done using the powercfg.exe, but I wanted to be sure it always set correctly. Plus, the GUID associated with a power scheme can be different, so I wanted to specify the power scheme by the name.

This PowerShell one-liner will set the power scheme on a machine to the scheme defined in the variable $Setting. If you do a powercfg.exe /l, you will see the name displayed to the right of the GUID in parenthesis. That is what you define in the above variable. The one-liner will then query powercfg to check if it matches the $Setting variable. If it does, it exits with an error code 0. If it does not match, then it sets the power scheme to the GUID from the query and then rechecks to make sure the setting was configured. If it still does not match, then it exits with an error code 1. 

To use this in a one-liner, you need to define the $Setting inside the one-liner below. This may differ on machines, so do the above query to see what is defined in your environment. Place this one-liner in a command line task sequence and you are done.


 powershell.exe -executionpolicy bypass -command "&{$Setting='Balanced';$Output = powercfg.exe /l | Where-Object {$_ -like ('*' + $Setting + '*')};If ($Output.Contains('*') -eq $true) {Write-Host ($Setting + [char]32 + 'is configured');Exit 0} else {$Output = powercfg.exe /s $output.split(' ')[3]; $Output = powercfg.exe /l | Where-Object {$_ -like ('*' + $Setting + '*')};If ($Output.Contains('*') -eq $true) {Write-Host ($Setting + [char]32 + 'Powercfg is configured');Exit 0} else {Write-Host 'Powercfg failed';Exit 1}}}"  

Below is a pic of what it looks like when used in a Run Command Line task sequence. IMO, it makes managing PowerShell scripts easier when they are contained within the task sequence.


15 April 2019

Configuring Wake-On-LAN for Dell Systems

If you have been wanting to wake your Dell systems up from sleep, hibernate, or shutdown states, this is how you do it. Starting out with this article from Dell, I got the list of things needed to set up the system for WOL. There are three areas that have to be configured on Dell systems, at least for the systems we have which range from the Optiplex 990 to the Latitude 7490. The areas are BIOS, advanced NIC, and power management settings. This site helped with the final setting to disable fast startup, which is required. WOL did not work on our systems until I implemented this final setting.

Before implementing this baseline, you will need to make sure Dell Command | Configure is installed on all systems. To ensure this, I have it deployed as an application to all Dell systems. Dell Command | Configure is what the baseline PowerShell scripts use to query and configure the BIOS settings. I also made a collection called All Dell Systems since we also have a few Microsoft Surfaces.

NOTE: This was created on April 15, 2019. New Dell models and BIOS updates are constantly released. It is likely there will be changes that need to be made in the future to these scripts to work with those updates.

The Wake-On-LAN Compliance item is set up to use a script that returns a Boolean value as shown below.


The discovery script is the following PowerShell script:


 ##Find Dell Command | Configure for 64-bit  
 $CCTK = Get-ChildItem -Path ${env:ProgramFiles(x86)}, $env:ProgramFiles -Filter cctk.exe -Recurse -ErrorAction SilentlyContinue | Where-Object {$_.Directory -like '*x86_64*'}  
 ##Get all available Dell Command | Configure commands for current system  
 $Commands = Invoke-Command -ScriptBlock {c:\Windows\system32\cmd.exe /c $CCTK.FullName -h} -ErrorAction SilentlyContinue  
 ##Configure BIOS --wakeonlan=enable  
 #Test if wakeonlan exists on current system  
 If ($Commands -like '*wakeonlan*') {  
      [string]$WakeOnLANSetting = 'wakeonlan=enable'  
      [string]$Output = Invoke-Command -ScriptBlock {c:\Windows\system32\cmd.exe /c $CCTK.FullName --wakeonlan} -ErrorAction SilentlyContinue  
      If ($Output -ne $WakeOnLANSetting) {  
           $WakeOnLAN = $false  
      } else {  
           $WakeOnLAN = $true  
      }  
 }  
 ##Configure BIOS --deepsleepctrl=disable  
 #Test if deepsleepctrl exists on current system  
 If ($Commands -like '*deepsleepctrl*') {  
      [string]$DeepSleepCtrlSetting = 'deepsleepctrl=disable'  
      [string]$Output = Invoke-Command -ScriptBlock {c:\Windows\system32\cmd.exe /c $CCTK.FullName --deepsleepctrl} -ErrorAction SilentlyContinue  
      If ($Output -ne $DeepSleepCtrlSetting) {  
           $DeepSleepCtrl = $false  
      } else {  
           $DeepSleepCtrl = $true  
      }  
 }  
 ##Configure BIOS --blocks3=disable  
 #Test if blocks3 exists on current system  
 If ($Commands -like '*blocks3*') {  
      [string]$BlockS3Setting = 'blocks3=disable'  
      [string]$Output = Invoke-Command -ScriptBlock { c:\Windows\system32\cmd.exe /c $CCTK.FullName --blocks3} -ErrorAction SilentlyContinue  
      If ($Output -ne $BlockS3Setting) {  
           $BlockS3 = $false  
      } else {  
           $BlockS3 = $true  
      }  
 }  
 ##Configure BIOS --cstatesctrl=disable  
 #Test if cstatesctrl exists on current system  
 If ($Commands -like '*cstatesctrl*') {  
      [string]$CStateCTRLSetting = 'cstatesctrl=disable'  
      [string]$Output = Invoke-Command -ScriptBlock { c:\Windows\system32\cmd.exe /c $CCTK.FullName --cstatesctrl} -ErrorAction SilentlyContinue  
      If ($Output -ne $CStateCTRLSetting) {  
           $CStateCTRL = $false  
      } else {  
           $CStateCTRL = $true  
      }  
 }  
 ##Disable Energy Efficient Ethernet  
 #Energy Efficient Ethernet disable registry value  
 $RegistryValue = '0'  
 #Find ethernet adapter  
 $Adapter = (Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}).Name  
 $DisplayName = (Get-NetAdapterAdvancedProperty -Name $Adapter | Where-Object {$_.DisplayName -like '*Efficient Ethernet*'}).DisplayName  
 #Test for presence of Energy-Efficient Ethernet  
 If ($DisplayName -like '*Efficient Ethernet*') {  
      [string]$CurrentState = (Get-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName).RegistryValue  
      If ($CurrentState -ne $RegistryValue) {  
           $EnergyEfficientEthernet = $false  
      } else {  
           $EnergyEfficientEthernet = $true  
      }  
 }  
 ##Enable Wake on Magic Packet  
 $State = 'Enabled'  
 $Adapter = (Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}).Name  
 $DisplayName = (Get-NetAdapterAdvancedProperty -Name $Adapter | Where-Object {$_.DisplayName -like '*Magic Packet*'}).DisplayName  
 #Test if Magic Packet exists  
 If ($DisplayName -like '*Magic Packet*') {  
      [string]$CurrentState = (Get-NetAdapterPowerManagement -Name $Adapter).WakeOnMagicPacket  
      If ($CurrentState -ne $State) {  
           $WakeOnMagicPacket = $false  
      } else {  
           $WakeOnMagicPacket = $true  
      }  
 }  
 ##Disable Shutdown Wake-On-Lan  
 $RegistryValue = '0'  
 $Adapter = (Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}).Name  
 $DisplayName = (Get-NetAdapterAdvancedProperty -Name $Adapter -ErrorAction SilentlyContinue | Where-Object {$_.DisplayName -eq 'Shutdown Wake-On-Lan'}).DisplayName  
 If ($DisplayName -eq 'Shutdown Wake-On-Lan') {  
      [string]$CurrentState = (Get-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName).RegistryValue  
      If ($CurrentState -ne $RegistryValue) {  
           $ShutdownWakeOnLAN = $false  
      } else {  
           $ShutdownWakeOnLAN = $true  
      }  
 }  
 ##Enable Allow the computer to turn off this device  
 $KeyPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002bE10318}\'  
 #Test if KeyPath exists  
 If ((Test-Path $KeyPath) -eq $true) {  
      $PnPValue = 256  
      $Adapter = Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}  
      foreach ($Entry in (Get-ChildItem $KeyPath -ErrorAction SilentlyContinue).Name) {  
           If ((Get-ItemProperty REGISTRY::$Entry).DriverDesc -eq $Adapter.InterfaceDescription) {  
                $Value = (Get-ItemProperty REGISTRY::$Entry).PnPCapabilities  
                If ($Value -ne $PnPValue) {  
                     $PowerManagement = $false  
                } else {  
                     $PowerManagement = $true  
                }  
           }  
      }  
 }  
 ##Disable Fast Startup  
 $KeyPath = "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Power"  
 #Test if KeyPath exists  
 If ((Test-Path -Path ('REGISTRY::' + $KeyPath)) -eq $true) {  
      If ((Get-ItemProperty -Path ('REGISTRY::' + $KeyPath)).HiberbootEnabled -eq 0) {  
           $FastStartup = $false  
      } else {  
           $FastStartup = $true  
      }  
 }  
 #Write-Host 'Wake-On-LAN:'$WakeOnLAN  
 #Write-Host 'Deep Sleep Control:'$DeepSleepCtrl  
 #Write-Host 'BlockS3:'$BlockS3  
 #Write-Host 'CState Control:'$CStateCTRL  
 #Write-Host 'Energy Efficient Ethernet:'$EnergyEfficientEthernet  
 #Write-Host 'Wake-On-Magic-Packet:'$WakeOnMagicPacket  
 #Write-Host 'Shutdown Wake-On-LAN:'$ShutdownWakeOnLAN  
 #Write-Host 'Allow Computer to Turn Off this Device:'$PowerManagement  
 If ((($WakeOnLAN -eq $null) -or ($WakeOnLAN -eq $true)) -and ($FastStartup -eq $false) -and (($DeepSleepCtrl -eq $null) -or ($DeepSleepCtrl -eq $true)) -and (($BlockS3 -eq $null) -or ($BlockS3 -eq $true)) -and (($CStateCTRL -eq $null) -or ($CStateCTRL -eq $true)) -and (($EnergyEfficientEthernet -eq $null) -or ($EnergyEfficientEthernet -eq $true)) -and (($WakeOnMagicPacket -eq $null) -or ($WakeOnMagicPacket -eq $true)) -and (($ShutdownWakeOnLAN -eq $null) -or ($ShutdownWakeOnLAN -eq $true)) -and (($PowerManagement -eq $null) -or ($PowerManagement -eq $true))) {  
      echo $true  
 } else {  
      echo $false  
 }  
   

The remediation script is as follows:


 ##Find Dell Command | Configure for 64-bit  
 $CCTK = Get-ChildItem -Path ${env:ProgramFiles(x86)}, $env:ProgramFiles -Filter cctk.exe -Recurse -ErrorAction SilentlyContinue | Where-Object {$_.Directory -like '*x86_64*'}  
 ##Get all available Dell Command | Configure commands for current system  
 $Commands = Invoke-Command -ScriptBlock {c:\Windows\system32\cmd.exe /c $CCTK.FullName -h} -ErrorAction SilentlyContinue  
 ##Configure BIOS --wakeonlan=enable  
 #Test if wakeonlan exists on current system  
 If ($Commands -like '*wakeonlan*') {  
      [string]$WakeOnLANSetting = 'wakeonlan=enable'  
      [string]$Output = Invoke-Command -ScriptBlock {c:\Windows\system32\cmd.exe /c $CCTK.FullName --wakeonlan} -ErrorAction SilentlyContinue  
      If ($Output -ne $WakeOnLANSetting) {  
           $ErrCode = (Start-Process -FilePath $CCTK.FullName -ArgumentList ('--' + $WakeOnLANSetting) -Wait -Passthru).ExitCode  
           If ($ErrCode -eq 0) {  
                $WakeOnLAN = $true  
           } elseif ($ErrCode -eq 119) {  
                $WakeOnLAN = $true  
           } else {  
                $WakeOnLAN = $false  
           }  
           Remove-Variable -Name ErrCode  
      } else {  
           $WakeOnLAN = $true  
      }  
      Remove-Variable -Name WakeOnLANSetting  
      Remove-Variable -Name Output  
 }  
 ##Configure BIOS --deepsleepctrl=disable  
 #Test if deepsleepctrl exists on current system  
 If ($Commands -like '*deepsleepctrl*') {  
      [string]$DeepSleepCtrlSetting = 'deepsleepctrl=disable'  
      [string]$Output = Invoke-Command -ScriptBlock {c:\Windows\system32\cmd.exe /c $CCTK.FullName --deepsleepctrl} -ErrorAction SilentlyContinue  
      If ($Output -ne $DeepSleepCtrlSetting) {  
           $ErrCode = (Start-Process -FilePath $CCTK.FullName -ArgumentList ('--' + $DeepSleepCtrlSetting) -Wait -Passthru).ExitCode  
           If ($ErrCode -eq 0) {  
                $DeepSleepCtrl = $true  
           } elseif ($ErrCode -eq 119) {  
                $DeepSleepCtrl = $true  
           } else {  
                $DeepSleepCtrl = $false  
           }  
           Remove-Variable -Name ErrCode  
      }  
      Remove-Variable -Name DeepSleepCtrlSetting  
      Remove-Variable -Name Output  
 }  
 ##Configure BIOS --blocks3=disable  
 #Test if blocks3 exists on current system  
 If ($Commands -like '*blocks3*') {  
      [string]$BlockS3Setting = 'blocks3=disable'  
      [string]$Output = Invoke-Command -ScriptBlock { c:\Windows\system32\cmd.exe /c $CCTK.FullName --blocks3} -ErrorAction SilentlyContinue  
      If ($Output -ne $BlockS3Setting) {  
           $ErrCode = (Start-Process -FilePath $CCTK.FullName -ArgumentList ('--' + $BlockS3Setting) -Wait -Passthru).ExitCode  
           If ($ErrCode -eq 0) {  
                $BlockS3 = $true  
           } elseif ($ErrCode -eq 119) {  
                $BlockS3 = $true  
           } else {  
                $BlockS3 = $false  
           }  
           Remove-Variable -Name ErrCode  
      } else {  
           $BlockS3 = $true  
      }  
      Remove-Variable -Name BlockS3Setting  
      Remove-Variable -Name Output  
 }  
 ##Configure BIOS --cstatesctrl=disable  
 #Test if cstatesctrl exists on current system  
 If ($Commands -like '*cstatesctrl*') {  
      [string]$CStateCTRLSetting = 'cstatesctrl=disable'  
      [string]$Output = Invoke-Command -ScriptBlock { c:\Windows\system32\cmd.exe /c $CCTK.FullName --cstatesctrl} -ErrorAction SilentlyContinue  
      If ($Output -ne $CStateCTRLSetting) {  
           $ErrCode = (Start-Process -FilePath $CCTK.FullName -ArgumentList ('--' + $CStateCTRLSetting) -Wait -Passthru).ExitCode  
           If ($ErrCode -eq 0) {  
                $CStateCTRL = $true  
           } elseif ($ErrCode -eq 119) {  
                $CStateCTRL = $true  
           } else {  
                $CStateCTRL = $false  
           }  
           Remove-Variable -Name ErrCode  
      } else {  
           $CStateCTRL = $true  
      }  
      Remove-Variable -Name CStateCTRLSetting  
      Remove-Variable -Name Output  
 }  
 ##Disable Energy Efficient Ethernet  
 #Energy Efficient Ethernet disable registry value  
 $RegistryValue = '0'  
 #Find ethernet adapter  
 $Adapter = (Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}).Name  
 $DisplayName = (Get-NetAdapterAdvancedProperty -Name $Adapter | Where-Object {$_.DisplayName -like '*Efficient Ethernet*'}).DisplayName  
 #Test for presence of Energy-Efficient Ethernet  
 If ($DisplayName -like '*Efficient Ethernet*') {  
      [string]$CurrentState = (Get-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName).RegistryValue  
      If ($CurrentState -ne $RegistryValue) {  
           Set-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName -RegistryValue $RegistryValue  
           Do {  
                Try {  
                     [string]$CurrentState = (Get-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName).RegistryValue  
                     $Err = $false  
                } Catch {  
                     $Err = $true  
                }  
           } While ($Err -eq $true)  
           If ($RegistryValue -eq $CurrentState) {  
                $EnergyEfficientEthernet = $true  
           } else {  
                $EnergyEfficientEthernet = $false  
           }  
           Remove-Variable -Name Err  
      } else {  
           $EnergyEfficientEthernet = $true  
      }  
      Remove-Variable -Name RegistryValue  
      Remove-Variable -Name Adapter  
      Remove-Variable -Name DisplayName  
      Remove-Variable -Name CurrentState  
 }  
 ##Enable Wake on Magic Packet  
 $State = 'Enabled'  
 $Adapter = (Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}).Name  
 $DisplayName = (Get-NetAdapterAdvancedProperty -Name $Adapter | Where-Object {$_.DisplayName -like '*Magic Packet*'}).DisplayName  
 #Test if Magic Packet exists  
 If ($DisplayName -like '*Magic Packet*') {  
      [string]$CurrentState = (Get-NetAdapterPowerManagement -Name $Adapter).WakeOnMagicPacket  
      If ($CurrentState -ne $State) {  
           Set-NetAdapterPowerManagement -Name $Adapter -WakeOnMagicPacket $State  
           Do {  
                Try {  
                     [string]$CurrentState = (Get-NetAdapterPowerManagement -Name $Adapter).WakeOnMagicPacket  
                     $Err = $false  
                } Catch {  
                     $Err = $true  
                }  
           } While ($Err -eq $true)  
           If ($State -eq $CurrentState) {  
                $WakeOnMagicPacket = $true  
           } else {  
                $WakeOnMagicPacket = $false  
           }  
           Remove-Variable -Name Err  
      } else {  
           $WakeOnMagicPacket = $true  
      }  
      Remove-Variable -Name State  
      Remove-Variable -Name Adapter  
      Remove-Variable -Name DisplayName  
      Remove-Variable -Name CurrentState  
 }  
 ##Disable Shutdown Wake-On-Lan  
 $RegistryValue = '0'  
 $Adapter = (Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}).Name  
 $DisplayName = (Get-NetAdapterAdvancedProperty -Name $Adapter -ErrorAction SilentlyContinue | Where-Object {$_.DisplayName -eq 'Shutdown Wake-On-Lan'}).DisplayName  
 If ($DisplayName -eq 'Shutdown Wake-On-Lan') {  
      [string]$CurrentState = (Get-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName).RegistryValue  
      If ($CurrentState -ne $RegistryValue) {  
           Set-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName -RegistryValue $RegistryValue  
           Do {  
                Try {  
                     [string]$CurrentState = (Get-NetAdapterAdvancedProperty -Name $Adapter -DisplayName $DisplayName).RegistryValue  
                     $Err = $false  
                } Catch {  
                     $Err = $true  
                }  
           } While ($Err -eq $true)  
           If ($RegistryValue -eq $CurrentState) {  
                $ShutdownWakeOnLAN = $true  
           } else {  
                $ShutdownWakeOnLAN = $false  
           }  
           Remove-Variable -Name Err  
      } else {  
           $ShutdownWakeOnLAN = $true  
      }  
      Remove-Variable -Name RegistryValue  
      Remove-Variable -Name Adapter  
      Remove-Variable -Name DisplayName  
      Remove-Variable -Name CurrentState  
 }  
 ##Enable Allow the computer to turn off this device  
 $KeyPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002bE10318}\'  
 #Test if KeyPath exists  
 If ((Test-Path $KeyPath) -eq $true) {  
      $PnPValue = 256  
      $Adapter = Get-NetAdapter | Where-Object {($_.Status -eq 'Up') -and ($_.PhysicalMediaType -eq '802.3')}  
      foreach ($Entry in (Get-ChildItem $KeyPath -ErrorAction SilentlyContinue).Name) {  
           If ((Get-ItemProperty REGISTRY::$Entry).DriverDesc -eq $Adapter.InterfaceDescription) {  
                $Value = (Get-ItemProperty REGISTRY::$Entry).PnPCapabilities  
                If ($Value -ne $PnPValue) {  
                     Set-ItemProperty -Path REGISTRY::$Entry -Name PnPCapabilities -Value $PnPValue -Force  
                     Disable-PnpDevice -InstanceId $Adapter.PnPDeviceID -Confirm:$false  
                     Enable-PnpDevice -InstanceId $Adapter.PnPDeviceID -Confirm:$false  
                     $Value = (Get-ItemProperty REGISTRY::$Entry).PnPCapabilities  
                }  
                If ($Value -eq $PnPValue) {  
                     $PowerManagement = $true  
                } else {  
                     $PowerManagement = $false  
                }  
                Remove-Variable -Name Value  
           }  
      }  
      Remove-Variable -Name PnPValue  
      Remove-Variable -Name Adapter  
      Remove-Variable -Name KeyPath  
 }  
 ##Disable Fast Startup  
 $KeyPath = "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Power"  
 #Test if KeyPath exists  
 If ((Test-Path -Path ('REGISTRY::' + $KeyPath)) -eq $true) {  
      Set-ItemProperty -Path ('REGISTRY::' + $KeyPath) -Name 'HiberbootEnabled' -Value 0  
      If ((Get-ItemProperty -Path ('REGISTRY::' + $KeyPath)).HiberbootEnabled -eq 0) {  
           $FastStartup = $false  
      } else {  
           $FastStartup = $true  
      }  
 }  
 #Write-Host 'Wake-On-LAN:'$WakeOnLAN  
 #Write-Host 'Deep Sleep Control:'$DeepSleepCtrl  
 #Write-Host 'BlockS3:'$BlockS3  
 #Write-Host 'CState Control:'$CStateCTRL  
 #Write-Host 'Energy Efficient Ethernet:'$EnergyEfficientEthernet  
 #Write-Host 'Wake-On-Magic-Packet:'$WakeOnMagicPacket  
 #Write-Host 'Shutdown Wake-On-LAN:'$ShutdownWakeOnLAN  
 #Write-Host 'Allow Computer to Turn Off this Device:'$PowerManagement  
 If ((($WakeOnLAN -eq $null) -or ($WakeOnLAN -eq $true)) -and ($FastStartup -eq $false) -and (($DeepSleepCtrl -eq $null) -or ($DeepSleepCtrl -eq $true)) -and (($BlockS3 -eq $null) -or ($BlockS3 -eq $true)) -and (($CStateCTRL -eq $null) -or ($CStateCTRL -eq $true)) -and (($EnergyEfficientEthernet -eq $null) -or ($EnergyEfficientEthernet -eq $true)) -and (($WakeOnMagicPacket -eq $null) -or ($WakeOnMagicPacket -eq $true)) -and (($ShutdownWakeOnLAN -eq $null) -or ($ShutdownWakeOnLAN -eq $true)) -and (($PowerManagement -eq $null) -or ($PowerManagement -eq $true))) {  
      echo $true  
 } else {  
      echo $false  
 }  
   


Finally, the compliance rule is as follows:


You may wonder why I included Remove-Variable cmdlets. I used those when I was debugging so it was easier to track variable values.

For the configuration baseline, I have it configured as shown below:


11 April 2019

Ensuring Compliance When Deploying a Self-Updating Application

In my list of recent security projects, I needed to ensure certain applications are present on systems by using SCCM application deployment. One of those applications was Dell Command | Configure. The issue with this application is the Dell Command | Update will update the application which in turn would register it as not installed to SCCM, thereby kicking off the installation again. That, in turn, would downgrade the application. There are three built-in options in SCCM to choose from that indicate whether an application is installed or not. Those are application GUID, files, and registry. The GUID typically changes every time an app is upgraded and the files and registry can change too. Luckily, this application never changes its name in the programs and features. The version field is typically what changes unless it is a significant upgrade.

The fourth option for confirming if an app is installed is custom method detection where you use a PowerShell script. That is the option I have used to make sure the Dell Command | Configure is registered as installed, no matter the version it has updated to. The following script can be used for this purpose. As you can see, I assigned the application name exactly as it appears in the programs and features to the variable $Application. If a company does include the version in the application name, then you can wildcard the version portion. Say the example below was Dell Command | Configure 3.1, you could use Dell Command | Configure for $Application and it would still find the app. You might wonder why I am outputting the name of the application. All SCCM wants to see is a string output which it interprets as installed. If no output occurs, then SCCM interprets that as not installed. 


 $Application = 'Dell Command | Configure'  
 $InstalledApps = Get-ChildItem -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Recurse | ForEach-Object {$_.GetValue('DisplayName')}  
 If (@($InstalledApps) -like ('*' + $Application + '*')) {  
      Write-Host (@($InstalledApps) -like $Application)  
 }  

01 April 2019

Running an SCCM Package via PowerShell and Command Line

While working on a new compliance policy, I ran into a lot of hurdles that needed to be resolved. One of those hurdles was executing an SCCM package via PowerShell. Using the WMIExplorer, I was able to locate a method that allows you to execute an SCCM package as shown below.


Once I located the namespace, class, and method, I needed to find out the name of the package in SCCM that I wanted to execute. To do this, the easiest method is to perform a WMI query in PowerShell on the advertised machine. The query is:

 Get-WmiObject -Class ccm_program -Namespace root\ccm\ClientSDK  

The results will display all advertised programs to that machine. From the results, locate the package you are wanting to execute by the Name, which will match the name in the SCCM console. Once you have found the package, take note of the PackageID and ProgramID, as these are the two items needed to execute the package via PowerShell.

The syntax of calling this from PowerShell is as follows, where the Program ID and Package ID are substituted with the appropriate data from the WMI query:

 ([wmiclass]'root\ccm\ClientSDK:CCM_ProgramsManager').ExecuteProgram('<ProgramID>','<Package ID>')  

The following PowerShell command line method will allow you to call this from the command line, where the Program ID and Package ID are substituted with the appropriate data from the WMI query:

 powershell.exe -executionpolicy bypass -command "&{([wmiclass]'root\ccm\ClientSDK:CCM_ProgramsManager').ExecuteProgram('<ProgramID>','<Package ID>')}"  

When executing the package, there will be output. If you allow for the default output, it can take an extended period of time to gather the information. Specifying a specific field will dramatically speed up the execution time.

NOTE: One thing I learned after discovering this is that SCCM Compliance rules cannot execute this WMI method. I will be writing a separate blog on that in the future as I just finished the compliance policy that goes into detail on executing an SCCM package. 

27 March 2019

Initiating an SCCM Compliance Check via PowerShell

Recently, I have been working on Configuration Baselines for security purposes. While doing so, two of my baselines required remediation that takes longer than 1 minute. I do not recall where I read it, but I believe the timeout for a compliance check is 1 minute. If the compliance remediation takes longer than 1 minute, then the baseline is designated as non-compliant until the next compliance check is run. This snippet of code can also be used in any other instance where the configuration manager client is installed.

To expedite this process, I tracked down how to execute a compliance check through PowerShell so that it can be executed at the end of the remediation script. Thanks to Trevor Sullivan's blog post, I was able to grab and modify the code from it to make into an easy to use code snippet within a Baseline remediation PowerShell script.

To make this easier, I wrote the script as two lines. The first line is where you specify the name of the baseline. As you can see in the pic below of a partial list of baseline configurations, the names of those baselines are what you need to specify for the variable $Name. The code snippet at the bottom shows using the Pending Reboot name to trigger a compliance check for that baseline. 



Once you have specified the name of the baseline, you can then copy and paste both lines at the bottom of the PowerShell remediation script so that a baseline configuration is triggered at the end of the remediation. Here is the code snippet: 


 $Name='Pending Reboot'  
 ([wmiclass]"root\ccm\dcm:SMS_DesiredConfiguration").TriggerEvaluation(((Get-WmiObject -Namespace root\ccm\dcm -class SMS_DesiredConfiguration | Where-Object {$_.DisplayName -eq $Name}).Name), ((Get-WmiObject -Namespace root\ccm\dcm -class SMS_DesiredConfiguration | Where-Object {$_.DisplayName -eq $Name}).Version))