PowerShell Example: Add KBs to Patch Groups

Goal

Provide an example PowerShell script that adds specific KBs to a new patch group or to existing patch groups. If you need to add an out-of-band security patch to many patch groups at once, this script simplifies the process.

Example Script

#################################################################################
#
#                               DISCLAIMER: EXAMPLE ONLY
#
# Execute this example at your own risk. The console, target machines and  
# databases should be backed up prior to executing this example. Ivanti does 
# not warrant that the functions contained in this example will be
# uninterrupted or free of error. The entire risk as to the results and
# performance of this example is assumed by the person executing the example.
# Ivanti is not responsible for any damage caused by this example.
#
#################################################################################
.SYNOPSIS
	Adds a specified list of KBs to a specified list of patch groups.

.PARAMETER KBs
	List of KBs that should be added to each of the inputted patch groups, ex. @('KB5010359', 'Q5004945', '5010427'). KBs already in the patch group will be ignored.
	Any existing patches in patch groups are not modified.

.PARAMETER PatchGroupName
	Name of patch group to add the provided KBs to. If patch group with specified name doesn't exist, one will be created.
	If these patch groups are associated with scan templates, this means the associated scan templates
	will take these newly-added KBs into account the next time a scan is run with the associated templates.

.PARAMETER ConsoleName
	Name of console that the inputted patch groups exist on.

.PARAMETER Port
	Port used for Security Controls REST API calls. Default is 3121.

.PARAMETER Credential
	Credential used to access remote machine. If omitted, will fall back to UseDefaultCredentials.

.PARAMETER ErrorPolicy
	How to handle errors when invalid KBs are passed in. Possible values are Throw and Omit. Throw will exit the script on the first invalid KB found, Omit, will skip over invalid
	KBs and continue to add the valid KBs.

.EXAMPLE
	.\Add-KBsToPatchGroups.ps1 -KBs @('KB5004945', 'KB5004953') -PatchGroupName 'MyPatchGroup' -ConsoleName 'MyConsole' -Credential Get-Credential
#>

param
(
	[Parameter(mandatory=$true, HelpMessage="List of KBs that should be added to each of the inputted patch groups, ex. @('KB5010359', 'Q5004945', '5010427').")]
	[Array] $KBs,
	[Parameter(mandatory=$true, HelpMessage="Name of patch group to add the provided KBs to. If patch group with specified name doesn't exist, one will be created.")]
	[String] $PatchGroupName,
	[Parameter(mandatory=$true, HelpMessage="Name of console that the inputted patch groups exist on.")]
	[String] $ConsoleName,
	[Parameter(Mandatory = $false, HelpMessage="Port used for Security Controls REST API calls. Default is 3121.")]
	[Int32] $Port = 3121,
	[Parameter(Mandatory = $false, HelpMessage="Credential used to access remote machine. If omitted, will fall back to UseDefaultCredentials.")]
	[PSCredential] $Credential,
	[Parameter(Mandatory = $false, HelpMessage="How to handle errors when invalid KBs are passed in. Possible values are Throw and Omit.")]
	[ValidateSet('Omit', 'Throw')]
	[String] $ErrorPolicy = 'Throw'
)
# Helper to handle REST operations.
function Invoke-RestCall
{
	param
	(
		# Part of URI that points to specific Security Controls REST operation.
		[String]$uriExtension,
		# REST method type.
		[String]$method = 'Get',
		# Body to pass with REST call.
		[object]$body = $null
	)
				
	$optionalParams = @{ }

	if ($null -ne $Credential)
	{
		$optionalParams.Add("Credential", $Credential)
	}
	else
	{
		$optionalParams.Add("UseDefaultCredentials", $null)
	}
				
	if ($null -ne $body)
	{
		if ($body.GetType().Name -ne "String")
		{
			$body = ConvertTo-Json $body
		}

		$optionalParams.Add("Body", $body)
	}
	
	$uri = "https://${ConsoleName}:$Port/st/console/api/v1.0/$uriExtension"

	return Invoke-RestMethod -Method $method -Uri $uri -ContentType 'application/json' @optionalParams
}

# URIs used throughout script
$patchesExtension = 'patches'
$patchGroupsExtension = 'patch/groups'

# Get list of all patches associated to the inputted KBs
$kbUrlList = [String]::Join(',', $kbs)
$patches = Invoke-RestCall ($patchesExtension + '?kbs=' + $kbUrlList + '&errorPolicy=' + $ErrorPolicy)

# With error policy Throw and at least one invalid KB provided, exit the script immediately
# Patches API itself will output message about which KBs are invalid
if ($null -eq $patches)
{
	Write-Error 'Exiting script due to invalid KB found or other error'
	exit
}

# With error policy Omit, caller needs to get list of invalid KBs
if ($patches.value.invalidIds.count -gt 0)
{
	$patches.value.invalidIds | ForEach-Object { Write-Warning ('Invalid KB found: ' + $_) }
}

# Edge case: Error policy Omit with every provided KB invalid
if ($null -eq $patches.value.vulnerabilities)
{
	Write-Error 'Exiting script due to no valid KBs found'
	exit
}

# Put together the list of IDs to pass to the patch group API later
$patchIds = New-Object System.Collections.Generic.List[System.String]
$patches.value.vulnerabilities | ForEach-Object {
	$patchIds.Add($_.id)
}

Write-Output ('Up to ' + $patchIds.count + ' patches will be added to patch group ' + $PatchGroupName)

# Find existing patch group if one exists
# If a patch group with the provided name doesn't exist, a new patch group will be created.
$existingPatchGroups = Invoke-RestCall ($patchGroupsExtension + '?name=' + $PatchGroupName)

# Patch group doesn't already exist, so create a new one
if ($existingPatchGroups.count -eq 0)
{
	$patchGroupBody = @{ name = $PatchGroupName }
	$newPatchGroup = Invoke-RestCall -uriExtension $patchGroupsExtension -Body $patchGroupBody -Method Post
	Write-Output ('Creating patch group ' + $PatchGroupName)
	$patchGroupId = $newPatchGroup.id
}
# Patch group exists, so use it
else
{
	Write-Output ('Using existing patch group ' + $PatchGroupName)
	$patchGroupId = $existingPatchGroups.value.id
}

# Add patch IDs to the patch group
	Write-Output ('Adding provided patches to group ' + $_)
	Invoke-RestCall -uriExtension ($patchGroupsExtension + '/' + $patchGroupId + '/patches') -Body $patchIds -method Post