This is my Console extension. There are many like it, but this one is mine.

Recently, I’ve been delving into console extensions and right click tools for SCCM. I built a couple basic ones then decided to tackle a long standing gripe with the software update node. So you know how in the All Software Updates node SCCM will tell you the number of devices that Require said update but then make you go run a report to actually get the list of devices?? I wanted a quick and easy way to get the list of devices without ever leaving the software update node. Thus started a frustrating journey into learning about SSRS report integration into powershell. I ended up settling on a version of the tool that works for my needs but could definitely use some upgrades. Here’s how I did it.

TL/DR

Also —- SCCM 1906 was just released and pretty much makes my extension obsolete because it has this functionality and more built in. I am to invested at this point to NOT post this. 🙂

Step 1: Can I even do this in powershell?
Yes of course you can. Well sort of, as long as you have the Microsoft Report Viewer assembly installed. I downloaded and installed from here. FYI, make sure to reboot after you install. I ignored the prompt to reboot and wasted some time chasing errors. You will also need Microsoft® System CLR Types for Microsoft® SQL Server® 2012. Giving credit, I leaned heavily on this blog post to get through this part.

Step 2: Which report do I need?
I am using Software Updates – A Compliance/Compliance 8 – Computers in a specific compliance state for an update (secondary) to get the data I need. This report requires 3 pieces of information to run; Update Name, Collection, and Update State.

Step 3: Testing in Powershell
We will need to add in some variables so your Powershell script knows where to run this report, namely the Report Server URL and the Report Path. You can find both of these in the SCCM console. Report Server URL is on the main reporting node and the path is in the properties of the report itself. Instead of running the report, right click and open properties then look in the Security tab.



You will also need to know the UpdateID from whichever update you are searching for. UpdateID is also known as CI_ID which will come into play later.

Here is the code I used to manually run the report and dump the results to a CSV. I saved it to C:\Scripts\GetUpdatesRequired.ps1 (this will be important later).

##

$CSVOutputFile = 'C:\temp\Output.csv'

## Load report viewer assemblies
		Add-Type -AssemblyName "Microsoft.ReportViewer.WinForms, Version=12.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"
		
		## Create a ReportViewer object
		
		$rv = New-Object Microsoft.Reporting.WinForms.ReportViewer
		
		$rv.ServerReport.ReportServerUrl = "http://SCCMServer/ReportServer"
		$rv.ServerReport.ReportPath = "/ConfigMgr_ABC/Software Updates - A Compliance/Compliance 8 - Computers in a specific compliance state for an update (secondary)"
		$rv.ProcessingMode = "Remote"
		
		$inputParams = @{
			"CollID"   = "SMS00001";
			"UpdateID" = "16838231";
			"Status"   = "Update is required"
		}
		
		#create an array based on how many incoming parameters 
		$params = New-Object 'Microsoft.Reporting.WinForms.ReportParameter[]' $inputParams.Count
		
		$i = 0
		foreach ($p in $inputParams.GetEnumerator())
		{
			$params[$i] = New-Object Microsoft.Reporting.WinForms.ReportParameter($p.Name, $p.Value, $false)
			$i++
		}
		
		$rv.ServerReport.SetParameters($params)
		# These variables are used for remdering PDF's. I left them in anyways.
		$mimeType = $null
		$encoding = $null
		$extension = $null
		$streamids = $null
		$warnings = $null
		
		$fileName = '$CSVOutputFile'
		$fileStream = New-Object System.IO.FileStream($fileName, [System.IO.FileMode]::OpenOrCreate)
		$fileStream.Write($bytes, 0, $bytes.Length)
		$fileStream.Close()
		
		
		# render the SSRS report in CSV 
		$bytes = $null
		$bytes = $rv.ServerReport.Render("CSV",
			$null,
			[ref]$mimeType,
			[ref]$encoding,
			[ref]$extension,
			[ref]$streamids,
			[ref]$warnings)
		
	
		# save the report to a file
		$fileStream = New-Object System.IO.FileStream($CSVOutputFile, [System.IO.FileMode]::OpenOrCreate)
		$fileStream.Write($bytes, 0, $bytes.Length)
		$fileStream.Close()
		
		# Re-import file and remove first nine lines
		
		get-content -LiteralPath $CSVOutputFile |
		select -Skip 9 |
		set-content "$CSVOutputFile-temp"
		move "$CSVOutputFile-temp" $CSVOutputFile -Force

##

Step 4: Now What?
So the script is ready but how do I add in a GUI? Well…I cheated and used a pre-built form in Powershell Studio 2019. Using PS2019 I was able to design\edit the form and then add my code in. You can also use the free site POSHGUI.com to design a form and export the code.

Step 5: Making a console extension
First off, HUGE shout out to Ryan Ephgrave for his blog post about creating your own Right Click tools. He really goes into depth explaining the how and even has a very clever tool that will show you which node of the SCCM console corresponds to the XML folder. I recommend stopping now and going to read that post.

So one of my biggest questions with this console extension is figuring out how to pass through the required Software Update to the Powershell script. Since you just read Ryan’s blog post I’m sure you know that the extension is powered off of a small XML file that lives in the GUID folder of the node you want to work with. Here is a screenshot of Ryan’s tool at work.

Notice how the GUID shows up under the properties of the update? Write that GUID down! Navigate to C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\XmlStorage\Extensions\Actions (assuming your console is installed on c:) and search for that GUID. Once you find it, drop in this XML file. Name it whatever you wish.

<ActionDescription Class="Executable" DisplayName="Which Devices Need This Update" MnemonicDisplayName="Which Devices Need This Update" Description="Which Devices Need This Update" SqmDataPoint="53">
    <ShowOn>
        <string>ContextMenu</string>
    </ShowOn>
    <ResourceAssembly>
        <Assembly>AdminUI.CollectionProperty.dll</Assembly>
            <Type>Microsoft.ConfigurationManagement.AdminConsole.CollectionProperty.Properties.Resources.resources</Type>
    </ResourceAssembly>
        <ImagesDescription>
            <ResourceAssembly>
                <Assembly>AdminUI.UIResources.dll</Assembly>
                <Type>Microsoft.ConfigurationManagement.AdminConsole.UIResources.Properties.Resources.resources</Type>
            </ResourceAssembly>
        <ImageResourceName>Information</ImageResourceName>
    </ImagesDescription>
    <Executable>
        <FilePath>"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"</FilePath>
        <Parameters>-noninteractive -windowstyle hidden -executionpolicy bypass -file "C:\Scripts\GetUpdatesRequired.ps1" -CI_ID "##SUB:CI_ID##"</Parameters>
    </Executable>
</ActionDescription>

Example of my xml file in its new home.

This XML file is the connection between the SCCM console and your Powershell script. Especially important is this bit here:

<FilePath>"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"</FilePath>
        <Parameters>-noninteractive -windowstyle hidden -executionpolicy bypass -file "C:\Scripts\GetUpdatesRequired.ps1" -CI_ID "##SUB:CI_ID##"</Parameters>

Notice that the file is calling Powershell.exe and kicking off the script we saved earlier. It also has an odd parameter named CI_ID which you should remember from our first script. This parameter is going to pass the ID of the Software Update directly to our script!! Since we are passing a variable we need to go back and edit our script and include $CI_ID parameter. We are also going to use $CI_ID to get the Unique ID and Display Name of the update. Here is the finalized script. You must edit the ReportServerURL and ReportPath variables to match your environment! Lines 36-47.

	[CmdletBinding()]
	param (
		[parameter(Mandatory = $true)]
		$CI_ID
	)

function Show-CMReportForm_psf {

	#----------------------------------------------
	#region Import the Assemblies
	#----------------------------------------------
	[void][reflection.assembly]::Load('System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
	[void][reflection.assembly]::Load('System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
	#endregion Import Assemblies

	#----------------------------------------------
	#region Generated Form Objects
	#----------------------------------------------
	[System.Windows.Forms.Application]::EnableVisualStyles()
	$formTex = New-Object 'System.Windows.Forms.Form'
	$labelHostname = New-Object 'System.Windows.Forms.Label'
	$richtextbox1 = New-Object 'System.Windows.Forms.RichTextBox'
	$buttonFind = New-Object 'System.Windows.Forms.Button'
	$textboxFind = New-Object 'System.Windows.Forms.TextBox'
	$buttonCopy = New-Object 'System.Windows.Forms.Button'
	$buttonExit = New-Object 'System.Windows.Forms.Button'
	$buttonLoad = New-Object 'System.Windows.Forms.Button'
	$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'
	#endregion Generated Form Objects

	#----------------------------------------------
	# User Generated Script
	#----------------------------------------------

# -------------------------------------------------------------------------------------------------------------
	# Manually Declare variables

    # Temp file that stores report
	$CSVOutputFile = 'c:\scripts\CMUpdateReport.csv'
    # Add your report server ex: http://SCCMServer/ReportServer
	$ReportServerURL = "http://SCCMServer/ReportServer"
    # Add your report path. Ex: /ConfigMgr_ABC/Software Updates - A Compliance/Compliance 8 - Computers in a specific compliance state for an update (secondary)
	$ReportPath = "/ConfigMgr_ABC/Software Updates - A Compliance/Compliance 8 - Computers in a specific compliance state for an update (secondary)"
	# Identity collection you want the report ran against. SMS00001 is the 'All Systems' collection
    $CMCollectionID = "SMS00001"
# -------------------------------------------------------------------------------------------------------------
	
	import-module ($Env:SMS_ADMIN_UI_PATH.Substring(0, $Env:SMS_ADMIN_UI_PATH.Length - 5) + '\ConfigurationManager.psd1')
	$Drive = Get-PSDrive -PSProvider CMSite
	CD "$($Drive):"
	
    $UpdateSource = Get-cmsoftwareupdate -ID $CI_ID -fast
	$ReqdUpdate = ($UpdateSource).CI_UniqueID
    [string]$CMUpdate = ($UpdateSource).LocalizedDisplayName

    # Remove old temp file if it exists
    if (test-path $CSVOutputFile) { Remove-Item –path $CSVOutputFile }	
	
	#region FindFunction
	function FindText
	{	
		if($textboxFind.Text.Length -eq 0)
		{
			return
		}
		
		$index = $richtextbox1.Find($textboxFind.Text,$richtextbox1.SelectionStart+ $richtextbox1.SelectedText.Length,[System.Windows.Forms.RichTextBoxFinds]::None)
		if($index -ge 0)
		{	
			$richtextbox1.Select($index,$textboxFind.Text.Length)
			$richtextbox1.ScrollToCaret()
			#$richtextbox1.Focus()
		}
		else
		{
			$index = $richtextbox1.Find($textboxFind.Text,0,$richtextbox1.SelectionStart,[System.Windows.Forms.RichTextBoxFinds]::None)
			#
			if($index -ge 0)
			{	
				$richtextbox1.Select($index,$textboxFind.Text.Length)
				$richtextbox1.ScrollToCaret()
				#$richtextbox1.Focus()
			}
			else
			{
				$richtextbox1.SelectionStart = 0	
			}
		}
		
	}
	#endregion
	
	$formTex_Load={
		#TODO: Initialize Form Controls here
		
	}
	
	$buttonExit_Click={
		Remove-Item –path $CSVOutputFile
		$formTex.Close()
	}
	
	$buttonLoad_Click={
		Update-Text
	}
	
	$buttonCopy_Click={
		$richtextbox1.SelectAll() #Select all the text
		$richtextbox1.Copy()	#Copy selected text to clipboard
		$richtextbox1.Select(0,0); #Unselect all the text
	}
	
	$textboxFind_TextChanged={
		$buttonFind.Enabled = $textboxFind.Text.Length -gt 0
	}
	
	$buttonFind_Click={
		FindText
	}
	
	#################################################
	# Customize LoadText Function
	#################################################
	
	function Update-Text
	{
	
		## Load report viewer assemblies
		Add-Type -AssemblyName "Microsoft.ReportViewer.WinForms, Version=12.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"
		
		## Create a ReportViewer object
		
		$rv = New-Object Microsoft.Reporting.WinForms.ReportViewer
		
		$rv.ServerReport.ReportServerUrl = $ReportServerURL
		$rv.ServerReport.ReportPath = $ReportPath
		$rv.ProcessingMode = "Remote"
		
		$inputParams = @{
			"CollID"   = $CMCollectionID;
			"UpdateID" = $ReqdUpdate;
			"Status"   = "Update is required"
		}
		
		#create an array based on how many incoming parameters 
		$params = New-Object 'Microsoft.Reporting.WinForms.ReportParameter[]' $inputParams.Count
		
		$i = 0
		foreach ($p in $inputParams.GetEnumerator())
		{
			$params[$i] = New-Object Microsoft.Reporting.WinForms.ReportParameter($p.Name, $p.Value, $false)
			$i++
		}
		
		$rv.ServerReport.SetParameters($params)
		# These variables are used for remdering PDF's. I left them in anyways.
		$mimeType = $null
		$encoding = $null
		$extension = $null
		$streamids = $null
		$warnings = $null
		
		$fileName = '$CSVOutputFile'
		$fileStream = New-Object System.IO.FileStream($fileName, [System.IO.FileMode]::OpenOrCreate)
		$fileStream.Write($bytes, 0, $bytes.Length)
		$fileStream.Close()
		
		
		# render the SSRS report in CSV 
		$bytes = $null
		$bytes = $rv.ServerReport.Render("CSV",
			$null,
			[ref]$mimeType,
			[ref]$encoding,
			[ref]$extension,
			[ref]$streamids,
			[ref]$warnings)
		
	
		# save the report to a file
		$fileStream = New-Object System.IO.FileStream($CSVOutputFile, [System.IO.FileMode]::OpenOrCreate)
		$fileStream.Write($bytes, 0, $bytes.Length)
		$fileStream.Close()
		
		# Re-import file and remove first nine lines
		
		get-content -LiteralPath $CSVOutputFile |
		select -Skip 9 |
		set-content "$CSVOutputFile-temp"
		move "$CSVOutputFile-temp" $CSVOutputFile -Force
		
		###################################################
		
	
		$Finalvalues = Import-CSV -LiteralPath $CSVOutputFile | select -ExpandProperty Details_Table0_ComputerName0
		$richtextbox1.Text = $Finalvalues | Format-Table | out-string
			
	}
	
	
	
	# --End User Generated Script--
	#----------------------------------------------
	#region Generated Events
	#----------------------------------------------
	
	$Form_StateCorrection_Load=
	{
		#Correct the initial state of the form to prevent the .Net maximized form issue
		$formTex.WindowState = $InitialFormWindowState
	}
	
	$Form_Cleanup_FormClosed=
	{
		#Remove all event handlers from the controls
		try
		{
			$buttonFind.remove_Click($buttonFind_Click)
			$textboxFind.remove_TextChanged($textboxFind_TextChanged)
			$buttonCopy.remove_Click($buttonCopy_Click)
			$buttonExit.remove_Click($buttonExit_Click)
			$buttonLoad.remove_Click($buttonLoad_Click)
			$formTex.remove_Load($formTex_Load)
			$formTex.remove_Load($Form_StateCorrection_Load)
			$formTex.remove_FormClosed($Form_Cleanup_FormClosed)
		}
		catch { Out-Null <# Prevent PSScriptAnalyzer warning #> }
	}
	#endregion Generated Events

	#----------------------------------------------
	#region Generated Form Code
	#----------------------------------------------
	$formTex.SuspendLayout()
	#
	# formTex
	#




	$formTex.Controls.Add($labelHostname)
	$formTex.Controls.Add($richtextbox1)
	$formTex.Controls.Add($buttonFind)
	$formTex.Controls.Add($textboxFind)
	$formTex.Controls.Add($buttonCopy)
	$formTex.Controls.Add($buttonExit)
	$formTex.Controls.Add($buttonLoad)
	$formTex.AcceptButton = $buttonFind
	$formTex.AutoScaleDimensions = '6, 13'
	$formTex.AutoScaleMode = 'Font'
	$formTex.ClientSize = '584, 362'
	$formTex.Name = 'formTex'
	$formTex.StartPosition = 'CenterScreen'
	$formTex.Text = "Devices Requiring $CMUpdate"
    $formTex.AutoSize = $True
	$formTex.add_Load($formTex_Load)
	#
	# labelHostname
	#
	$labelHostname.AutoSize = $True
	$labelHostname.Location = '12, 16'
	$labelHostname.Name = 'labelHostname'
	$labelHostname.Size = '56, 17'
	$labelHostname.TabIndex = 7
	$labelHostname.Text = 'Hostname'
	$labelHostname.UseCompatibleTextRendering = $True
	#
	# richtextbox1
	#
	$richtextbox1.Anchor = 'Top, Bottom, Left, Right'
	$richtextbox1.BackColor = 'Window'
	$richtextbox1.Font = 'Courier New, 8.25pt'
	$richtextbox1.HideSelection = $False
	$richtextbox1.Location = '12, 36'
	$richtextbox1.Name = 'richtextbox1'
	$richtextbox1.ReadOnly = $True
	$richtextbox1.RightToLeft = 'No'
	$richtextbox1.Size = '559, 281'
	$richtextbox1.TabIndex = 6
	$richtextbox1.Text = ''
	$richtextbox1.WordWrap = $False
	#
	# buttonFind
	#
	$buttonFind.Anchor = 'Top, Right'
	$buttonFind.Enabled = $False
	$buttonFind.Location = '536, 8'
	$buttonFind.Name = 'buttonFind'
	$buttonFind.Size = '36, 23'
	$buttonFind.TabIndex = 5
	$buttonFind.Text = '&amp;Find'
	$buttonFind.UseCompatibleTextRendering = $True
	$buttonFind.UseVisualStyleBackColor = $True
	$buttonFind.add_Click($buttonFind_Click)
	#
	# textboxFind
	#
	$textboxFind.Anchor = 'Top, Right'
	$textboxFind.Location = '339, 10'
	$textboxFind.Name = 'textboxFind'
	$textboxFind.Size = '191, 20'
	$textboxFind.TabIndex = 4
	$textboxFind.add_TextChanged($textboxFind_TextChanged)
	#
	# buttonCopy
	#
	$buttonCopy.Anchor = 'Bottom'
	$buttonCopy.Location = '255, 327'
	$buttonCopy.Name = 'buttonCopy'
	$buttonCopy.Size = '75, 23'
	$buttonCopy.TabIndex = 3
	$buttonCopy.Text = '&amp;Copy'
	$buttonCopy.UseCompatibleTextRendering = $True
	$buttonCopy.UseVisualStyleBackColor = $True
	$buttonCopy.add_Click($buttonCopy_Click)
	#
	# buttonExit
	#
	$buttonExit.Anchor = 'Bottom, Right'
	$buttonExit.Location = '501, 327'
	$buttonExit.Name = 'buttonExit'
	$buttonExit.Size = '75, 23'
	$buttonExit.TabIndex = 2
	$buttonExit.Text = 'E&amp;xit'
	$buttonExit.UseCompatibleTextRendering = $True
	$buttonExit.UseVisualStyleBackColor = $True
	$buttonExit.add_Click($buttonExit_Click)
	#
	# buttonLoad
	#
	$buttonLoad.Anchor = 'Bottom, Left'
	$buttonLoad.Location = '12, 327'
	$buttonLoad.Name = 'buttonLoad'
	$buttonLoad.Size = '75, 23'
	$buttonLoad.TabIndex = 1
	$buttonLoad.Text = '&amp;Load'
	$buttonLoad.UseCompatibleTextRendering = $True
	$buttonLoad.UseVisualStyleBackColor = $True
	$buttonLoad.add_Click($buttonLoad_Click)
	$formTex.ResumeLayout()
	#endregion Generated Form Code

	#----------------------------------------------

	#Save the initial state of the form
	$InitialFormWindowState = $formTex.WindowState
	#Init the OnLoad event to correct the initial state of the form
	$formTex.add_Load($Form_StateCorrection_Load)
	#Clean up the control events
	$formTex.add_FormClosed($Form_Cleanup_FormClosed)
	#Show the Form
	return $formTex.ShowDialog()

} #End Function

#Call the form
Show-CMReportForm_psf | Out-Null

Assuming you have saved the xml file to the right folder and saved the powershell script to C:\Scripts\GetUpdatesRequired.ps1 you are ready to test. (In your test environment only please).

Close and re-open the console. Go to software updates. Choose one that multiple machines require and right click it. HOPEFULLY your screen looks something like this.

Select Which Devices Need This Update and you should get a form similar too this.

Click Load and your devices should start to appear. The cool thing is you can click Copy and the entire list gets copied to your clipboard. You can also search for specific devices if it’s a long list.

TL/DR version.

1. Download the files below and change their extensions to ps1 and xml.
2. Edit the variables in the powershell file. (lines 36-47)
3. Copy the xml to C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\XmlStorage\Extensions\Actions\5360fd7a-a1c4-428f-91c9-89a4c5565ce1.
4. Close and re-open the console.
5. Save powershell script to C:\Scripts\GetUpdatesRequired.ps1.
6. Navigate to All Updates node and find an update that shows deviced need it.
7. Right click on the update and select Which Devices Need This Update.
8. Click Load.
9. Say ‘Wow that’s cool and go back to your conference call’.

This post turned out to be way longer than I originally wanted it to be and I glossed over several areas. Let me know if you have any questions\suggestions\improvements.

Update – I started having issues with my right clicks being terribly slow when right clicking on a device. This turned out to be a compatibility issues with Roger Zanders Client Center. Apparently we both have a script named New Test Collection. I uninstalled Client Center and the issue went away.

Leave a Reply