PowerShell Example: Scan for and Deploy Patches Using Input From a CVE File

Goal

Provide example PowerShell scripts that invoke the REST API and that perform a number of tasks, including:

Parse a CVE file and convert the content to a patch group

Create a scan template that scans for the patches contained in the patch group

Optionally deploy any missing patches

Master Script

This script calls two other scripts named Import-CvesToPatchGroup.ps1 and Invoke-ScanAndDeployPatchGroup.ps1 that are provided later in this topic. You can view the built-in help for any of the three scripts by using the Get-Help command. For example, copy the code to a file and then type the following at a PowerShell prompt:

PS C:\> Get-Help .\<scriptfilename.ps1> -detailed

 

########################################################################################
#
#                               DISCLAIMER
#
# This example is supported by Ivanti. If you have any issues when running this script, they can be
# reported to Ivanti Support for investigation. Executing this script will change the state of your
# your target machines and may cause the target machines to reboot. To mitigate any risks, before
# executing this example you should create a backup of your database and verify your target
# machines. Because each environment is unique, Ivanti does not warrant that the functions
# contained in this example will be uninterrupted or free of error. Ivanti is not responsible
# for damage caused by this example. The risk as to the results and performance of this example
# is assumed by the person executing this example.
#
########################################################################################
# 
<#
	.SYNOPSIS
		This script demonstrates the use of both the 'Import-CvesToPatchGroup.ps1' and 'Invoke-ScanAndDeployPatchGroup.ps1' scripts.
		This script will take a CVE file and from it build a patch group which will be scanned and optionally deployed to.
		Requires having both the 'Import-CvesToPatchGroup.ps1' and 'Invoke-ScanAndDeployPatchGroup.ps1' scripts in the current directory.
		Requires Powershell 5.1.

	.PARAMETER CveFilePath
		File Path of CVE file.

	.PARAMETER PatchGroupName
		Name of patch group. If the patch group does not exist, a new one will be created.
		If the patch group exists, the CVEs will be appended to the existing group.

	.PARAMETER ScanTemplateName
		The name of the scan template.
		The script will create a new scan template with the name provided.
		The scan template will scan for the patches associated to the patch group ID input parameter.
		If the scan template with this name already exists, it will be used. To overwrite an existing scan template with this name, apply the Force switch.

	.PARAMETER DeploymentTemplateId
		The ID to an existing deployment template that will be used for deploying the patches found in the scan.
		If a deployment template ID is not provided, the patches will not be deployed.

	.PARAMETER ConsoleName
		The name of the console where the patch group will be created.

	.PARAMETER EndpointMachineNames
		The list of machine names that will be scanned or deployed to.
		Only machine names and IP addresses are accepted.

	.PARAMETER EndpointsCredentialFriendlyName
		The friendly name for the endpoint credential.
		If this parameter is passed in but EndpointsCredential is not, the script will attempt to use an existing credential associated to EndpointsCredentialFriendlyName.
		If both this parameter and EndpointsCredential are passed in, a new credential by the name of EndpointsCredentialFriendlyName will be created.
		If both this parameter and EndpointsCredential are passed in and a credential by the name of EndpointsCredentialFriendlyName already exists, an error will be returned. To overwrite the existing credential, apply the Force switch.
		If neither this parameter nor EndpointsCredential are passed in, the Default Credential within Security Controls or session credential will be used if no Default is set.

	.PARAMETER EndpointsCredential
		The credential passed in for the endpoint machines. The type must be PSCredential.
		If this parameter is passed in but EndpointsCredentialFriendlyName is not, the script will create a new credential with a generic friendly name "API Machine Group Endpoint Credential".
		If both this parameter and EndpointsCredentialFriendlyName are passed in, a new credential by the name of EndpointsCredentialFriendlyName will be created.
		If both this parameter and EndpointsCredentialFriendlyName are passed in and a credential by the name of EndpointsCredentialFriendlyName already exists, an error will be returned. To overwrite the existing credential, apply the Force switch.
		If neither this parameter nor EndpointsCredential are passed in, the Default Credential within Security Controls or session credential will be used if no Default is set.

	.PARAMETER SessionCredential
		The credential used to authenticate as an administrator with capabilities of decrypting the endpoint credentials.
		This parameter will overwrite the existing session credential.
		This credential is an administrator on the console machine. This credential is used to:
			1) Authenticate to the console machine REST API.
			2) Used as the session credential on the console.
			3) Used as the credential that starts the scan and deployments.

	The following will happen if force switch is used:
			1) If both EndpointsCredentialFriendlyName and EndpointsCredential are passed in, the script will overwrite the existing endpoint credential associated to EndpointsCredentialFriendlyName.
			2) If a scan template already exists that associates with ScanTemplateName, the scan template will be overwritten.
				
	.EXAMPLE
		$InformationPreference = 'continue'
		$endpointCred = Get-Credential
		$sessionCred = Get-Credential
		$endpoints = @( "machineName1", "65.207.81.117" )

		.\Invoke-ScanAndDeployCveDocument.ps1 -CveFilePath .\CveFilePathHere.xml  -PatchGroupName 'PatchGroupNameHere' -ScanTemplateName 'ScanTemplateNameHere' -DeploymentTemplateId '<GuidHere>' -ConsoleName 'ConsoleMachineName' -EndpointMachineNames $endpoints -EndpointsCredential $endpointCred -SessionCredential $sessionCred

#>

param
(
	# File Path of CVE file.
	[Parameter(Mandatory = $true)]
	[ValidateScript( { Test-Path $_ -PathType Leaf })] 
	[String]$CveFilePath,

	# Name of patch group. If the patch group does not exist, a new one will be created.
	# If the patch group exists, the CVEs will be appended to the existing group.
	[Parameter(Mandatory = $true)]
	[ValidateScript( { -Not [String]::IsNullOrWhitespace($_) })] 
	[String]$PatchGroupName,

	# The name of the scan template.
	# The script will create a new scan template with the name provided.
	# The scan template will scan for the patches associated to the patch group ID input parameter.
	# If the scan template with this name already exists, it will be used. To overwrite an existing scan template with this name, apply the Force switch.
	[Parameter(Mandatory = $true)]
	[String]$ScanTemplateName,

	# The ID to an existing deployment template that will be used for deploying the patches found in the scan.
	# If a deployment template ID is not provided, the patches will not be deployed.
	[Parameter(Mandatory = $false)]
	[String]$DeploymentTemplateId,

	# The name of the console where the patch group will be created.
	[Parameter(Mandatory = $true)]
	[String]$ConsoleName,

	# The list of machine names that will be scanned or deployed to.
	# Only machine names and IP addresses are accepted.
	[Parameter(Mandatory = $true)]
	[String[]]$EndpointMachineNames,

	# The friendly name for the endpoint credential.
	# If this parameter is passed in but EndpointsCredential is not, the script will attempt to use an existing credential associated to EndpointsCredentialFriendlyName.
	# If both this parameter and EndpointsCredential are passed in, a new credential by the name of EndpointsCredentialFriendlyName will be created.
	# If both this parameter and EndpointsCredential are passed in and a credential by the name of EndpointsCredentialFriendlyName already exists, an error will be returned. To overwrite the existing credential, apply the Force switch.
	# If neither this parameter nor EndpointsCredential are passed in, the Default Credential within Security Controls or session credential will be used if no Default is set.
	[Parameter(Mandatory = $false)]
	[String]$EndpointsCredentialFriendlyName,
										
	# The credential passed in for the endpoint machines. The type must be PSCredential.
	# If this parameter is passed in but EndpointsCredentialFriendlyName is not, the script will create a new credential with a generic friendly name "API Machine Group Endpoint Credential".
	# If both this parameter and EndpointsCredentialFriendlyName are passed in, a new credential by the name of EndpointsCredentialFriendlyName will be created.
	# If both this parameter and EndpointsCredentialFriendlyName are passed in and a credential by the name of EndpointsCredentialFriendlyName already exists, an error will be returned. To overwrite the existing credential, apply the Force switch.
	# If neither this parameter nor EndpointsCredential are passed in, the Default Credential within Security Controls or session credential will be used if no Default is set.
	[Parameter(Mandatory = $false)]
	[PSCredential]$EndpointsCredential,

	# The credential used to authenticate as an administrator with capabilities of decrypting the endpoint credentials.
	# This parameter will overwrite the existing session credential.
	# This credential is an administrator on the console machine. This credential is used to:
	# 1) Authenticate to the console machine REST API.
	# 2) Used as the session credential on the console.
	# 3) Used as the credential that starts the scan and deployments.
	[Parameter(Mandatory = $true)]
	[PSCredential]$SessionCredential,

	# The following will happen if force switch is used:
	# 1) If both EndpointsCredentialFriendlyName and EndpointsCredential are passed in, the script will overwrite the existing endpoint credential associated to EndpointsCredentialFriendlyName.
	# 2) If a scan template already exists that associates with ScanTemplateName, the scan template will be overwritten.
	[Parameter(Mandatory = $false)]
	[Switch]$Force
)

##########################################################################################################################################
# Start Script
##########################################################################################################################################

$patchGroup = .\Import-CvesToPatchGroup.ps1 -FilePath $CveFilePath -PatchGroupName $PatchGroupName -ConsoleName $ConsoleName -Credential $SessionCredential
if ($Force.IsPresent)
{
	.\Invoke-ScanAndDeployPatchGroup.ps1 -PatchGroupId $patchGroup.Id -ScanTemplateName $ScanTemplateName -DeploymentTemplateId $DeploymentTemplateId -EndpointMachineNames $EndpointMachineNames -EndpointsCredentialFriendlyName $EndpointsCredentialFriendlyName -EndpointsCredential $EndpointsCredential -ConsoleName $ConsoleName -SessionCredential $SessionCredential -Force
}
else
{
	.\Invoke-ScanAndDeployPatchGroup.ps1 -PatchGroupId $patchGroup.Id -ScanTemplateName $ScanTemplateName -DeploymentTemplateId $DeploymentTemplateId -EndpointMachineNames $EndpointMachineNames -EndpointsCredentialFriendlyName $EndpointsCredentialFriendlyName -EndpointsCredential $EndpointsCredential -ConsoleName $ConsoleName -SessionCredential $SessionCredential
}
			

 

Example Script: Import-CvesToPatchGroup.ps1

This scripts parses a CVE file and converts its contents into a patch group.

<#
	.SYNOPSIS
		This script can be used to parse a file containing CVE's and convert them into a patch group.
		Requires Powershell 5.1.

	.PARAMETER FilePath
		File Path of CVE file.

	.PARAMETER PatchGroupName
		Name of patch group. If the patch group does not exist, a new one will be created.
		If the patch group exists, the CVEs will be appended to the existing group.

	.PARAMETER PatchGroupPath
		Patch group path. Will default to no path.

	.PARAMETER ConsoleName
		The name of the console where the patch group will be created.

	.PARAMETER Port
		The port that is used for REST call. The default port is 3121.

	.PARAMETER Credential
		The credential passed in. The type must be PSCredential. The default will be -UseDefaultCredential.

	.PARAMETER LogFilePath
		Log file path. The default log file path will be the current file path location.

	.EXAMPLE
		$InformationPreference = 'continue'
		.\Import-CvesToPatchGroup.ps1 -FilePath C:\MyCveFile.xml -PatchGroupName MyPatchGroup -ConsoleName MyConsoleName

#>

param
(
	# File Path of CVE file.
	[Parameter(Mandatory = $true)]
	[ValidateScript( { Test-Path $_ -PathType Leaf })] 
	[String]$FilePath,

	# Name of patch group. If the patch group does not exist, a new one will be created.
	# If the patch group exists, the CVEs will be appended to the existing group.
	[Parameter(Mandatory = $true)]
	[ValidateScript( { -Not [String]::IsNullOrWhitespace($_) })] 
	[String]$PatchGroupName,

	# Patch group path. Will default to no path.
	[Parameter(Mandatory = $false)]
	[String]$PatchGroupPath,

	# The name of the console where the patch group will be created.
	[Parameter(Mandatory = $true)]
	[String]$ConsoleName,

	# The port that is used for REST call. The default port is 3121.
	[Parameter(Mandatory = $false)]
	[Int32]$Port = 3121,

	# The credential passed in. The type must be PSCredential. The default will be -UseDefaultCredential.
	[Parameter(Mandatory = $false)]
	[PSCredential]$Credential,

	# Log file path. The default log file path will be the current file path location.
	[Parameter(Mandatory = $false)]
	[ValidateScript( { Test-Path $_ -PathType Leaf })]
	[String]$LogFilePath = '.\Import-CvesToPatchGroup.log'
)

function Read-CvesToHashSetFromFile
{
	param
	(
		[String]$filePath
	)

	$cveHashSet = [System.Collections.Generic.HashSet[String]]@()
	$options = [Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [Text.RegularExpressions.RegexOptions]::Multiline -bor [Text.RegularExpressions.RegexOptions]::Compiled
	$regex = '(CVE|CAN)-(1999|2\d{3})-(0\d{3}(?!\d)|[1-9]\d{3,})'
	foreach ($line in Get-Content $filePath)
	{
		$match = [regex]::Match($line, $regex, $options)
		if ($match.Success -eq $true)
		{
			$cveHashSet.Add($match.Value.ToUpper()) | Out-Null
		}
}

	if ($cveHashSet.Count -gt 0)
	{
		Write-Information "$($cveHashSet.Count) CVE(s) were successfully parsed from given file path."
	}
	else
	{
		Write-Information "No CVE's were found in given file path."
		exit
	}

	return $cveHashSet
}

function Get-PatchGroupID
{
	param
	(
		[String]$patchGroupName,
		[String]$patchGroupPath,
		[String]$consoleName,
		[Int32]$port,
		[PSCredential]$credential
	)

	$patchGroupId = Get-PatchGroupIDIfPatchGroupExists -patchGroupName $patchGroupName -consoleName $consoleName -port $port -credential $credential
	if ($null -eq $patchGroupId)
	{
		$patchGroupId = New-EmptyPatchGroup -patchGroupName $patchGroupName -patchGroupPath $patchGroupPath -consoleName $consoleName -port $port -credential $credential
	}

	return $patchGroupId
}

function Get-PatchGroupIDIfPatchGroupExists
{
	param
	(
		[String]$patchGroupName,
		[String]$consoleName,
		[Int32]$port,
		[PSCredential]$credential
	)

	$uriExtension = "patch/groups?Name=$patchGroupName"
	$results = Invoke-RestCall -uriExtension $uriExtension -method GET -consoleName $consoleName -port $port -credential $credential
	if ($results.count -eq 0)
	{
		return $null
	}

	Write-Information "A patch group was found with the given patch group name and will be used."

	return $results.value[0].id
}

function New-EmptyPatchGroup
{
	param
	(
		[String]$patchGroupName,
		[String]$patchGroupPath,
		[String]$consoleName,
		[Int32]$port,
		[PSCredential]$credential
	)

	$uriExtension = "patch/groups"
	$patchGroupBody = @{ Name = $patchGroupName; Path = $patchGroupPath }
	$result = Invoke-RestCall -uriExtension $uriExtension -method POST -body $patchGroupBody -consoleName $consoleName -port $port -credential $credential
	Write-Information "A new patch group was created with the given patch group name."

	return $result.id
}

function Add-CvesToPatchGroup
{
	param
	(
		[Int32]$patchGroupId,
		[System.Collections.Generic.HashSet[String]]$cveHashSet,
		[String]$consoleName,
		[Int32]$port,
		[PSCredential]$credential
	)

	$uriExtension = "patch/groups/$patchGroupId/patches/cves"
	$cveBody = @{ ErrorPolicy = 'Omit'; Cves = @($cveHashSet) }
	Invoke-RestCall -uriExtension $uriExtension -method POST -body $cveBody -consoleName $consoleName -port $port -credential $credential
}

function Invoke-RestCall
{
	param
	(
		[String]$uriExtension,
		[String]$method,
		[object]$body,
		[String]$consoleName,
		[Int32]$port,
		[PSCredential]$credential
	)

	$optionalParams = @{ }

	if (-not($null -eq $credential))
	{
		$optionalParams.Add("Credential", $credential)
	}
	else
	{
		$optionalParams.Add("UseDefaultCredentials", $null)
	}

	if (-not ($null -eq $body))
	{
		if (-not ($body.GetType().Name -eq "String"))
		{
			$body = ConvertTo-JSon $body -Depth 99
		}

		$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
}

Start-Transcript -Append "$LogFilePath" | Out-Null
try
{
	if ($null -eq $credential)
	{
		Write-Information "No credentials were provided so will use -UseDefaultCredentials switch."
	}
	Write-Information "Using port $port."
	$cveHashSet = Read-CvesToHashSetFromFile -filePath $FilePath
	$patchGroupId = Get-PatchGroupID -patchGroupName $PatchGroupName -patchGroupPath $PatchGroupPath -consoleName $ConsoleName -port $Port -credential $Credential

	Add-CvesToPatchGroup -patchGroupId $patchGroupId -cveHashSet $cveHashSet -consoleName $ConsoleName -port $Port -credential $Credential
}
catch [Exception]
{
	$private:e = $_.Exception
	do
	{
		Write-Error -Exception $private:e
		$private:e = $private:e.InnerException
	}
	while ($null -ne $private:e)
}
finally
{
	Stop-Transcript | Out-Null
}
			

 

Example Script: Invoke-ScanAndDeployPatchGroup.ps1

This script will scan a patch group and optionally deploy any missing patches.

	
<#
	.SYNOPSIS
		This script creates a scan template which scans for the patches contained in the patch group sent in. 
		The patches can then be optionally deployed depending on if the deployment switch is present or not.
		Requires Powershell 5.1.

	.PARAMETER PatchGroupId
		The ID to an existing patch group which a scan operation will be performed on.

	.PARAMETER ScanTemplateName
		The name of the scan template.
		The script will create a new scan template with the name provided.
		The scan template will scan for the patches associated to the patch group ID input parameter.
		If the scan template with this name already exists, it will be used. To overwrite an existing scan template with this name, apply the Force switch.

	.PARAMETER DeploymentTemplateId
		The ID to an existing deployment template that will be used for deploying the patches found in the scan.
		If a deployment template ID is not provided, the patches will not be deployed.

	.PARAMETER ConsoleName
		The name of the machine where Security Controls is installed.

	.PARAMETER EndpointMachineNames
		The list of machine names that will be scanned or deployed to.
		Only machine names and IP addresses are accepted.

	.PARAMETER EndpointsCredentialFriendlyName
		The friendly name for the endpoint credential.
		If this parameter is passed in but EndpointsCredential is not, the script will attempt to use an existing credential associated to EndpointsCredentialFriendlyName.
		If both this parameter and EndpointsCredential are passed in, a new credential by the name of EndpointsCredentialFriendlyName will be created.
		If both this parameter and EndpointsCredential are passed in and a credential by the name of EndpointsCredentialFriendlyName already exists, an error will be returned. To overwrite the existing credential, apply the Force switch.
		If neither this parameter nor EndpointsCredential are passed in, the Default Credential within Security Controls or session credential will be used if no Default is set.

	.PARAMETER EndpointsCredential
		The credential passed in for the endpoint machines. The type must be PSCredential.
		If this parameter is passed in but EndpointsCredentialFriendlyName is not, the script will create a new credential with a generic friendly name "API Machine Group Endpoint Credential".
		If both this parameter and EndpointsCredentialFriendlyName are passed in, a new credential by the name of EndpointsCredentialFriendlyName will be created.
		If both this parameter and EndpointsCredentialFriendlyName are passed in and a credential by the name of EndpointsCredentialFriendlyName already exists, an error will be returned. To overwrite the existing credential, apply the Force switch.
		If neither this parameter nor EndpointsCredential are passed in, the Default Credential within Security Controls or session credential will be used if no Default is set.

	.PARAMETER SessionCredential
		The credential used to authenticate as an administrator with capabilities of decrypting the endpoint credentials.
		This parameter will overwrite the existing session credential.
		This credential is an administrator on the console machine. This credential is used to:
		1) Authenticate to the console machine REST API.
		2) Used as the session credential on the console.
		3) Used as the credential that starts the scan and deployments.

	.PARAMETER Force
		The following will happen if force switch is used:
		1) If both EndpointsCredentialFriendlyName and EndpointsCredential are passed in, the script will overwrite the existing endpoint credential associated to EndpointsCredentialFriendlyName.
		2) If a scan template already exists that associates with ScanTemplateName, the scan template will be overwritten.

	.PARAMETER LogFilePath
		Log file path. The default log file path will be the current file path location.

	.EXAMPLE
		$InformationPreference = 'continue'
		$endpointCred = Get-Credential
		$sessionCred = Get-Credential
		$endpoints = @( "machineName1", "65.207.81.117" )

		.\Invoke-ScanAndDeployPatchGroup.ps1 -PatchGroupId 6 -ConsoleName 'ConsoleMachineName' -EndpointMachineNames $endpoints -EndpointsCredential $endpointCred -SessionCredential $sessionCred

#>

param
(
	# The ID to an existing patch group which a scan operation will be performed on.
	[Parameter(Mandatory = $true)]
	[String]$PatchGroupId,

	# The name of the scan template.
	# The script will create a new scan template with the name provided.
	# The scan template will scan for the patches associated to the patch group ID input parameter.
	# If the scan template with this name already exists, it will be used. To overwrite an existing scan template with this name, apply the Force switch.
	[Parameter(Mandatory = $true)]
	[String]$ScanTemplateName,

	# The ID to an existing deployment template that will be used for deploying the patches found in the scan.
	# If a deployment template ID is not provided, the patches will not be deployed.
	[Parameter(Mandatory = $false)]
	[String]$DeploymentTemplateId,

	# The name of the machine where Security Controls is installed.
	[Parameter(Mandatory = $true)]
	[String]$ConsoleName,

	# The list of machine names that will be scanned or deployed to.
	# Only machine names and IP addresses are accepted.
	[Parameter(Mandatory = $true)]
	[String[]]$EndpointMachineNames,

	# The friendly name for the endpoint credential.
	# If this parameter is passed in but EndpointsCredential is not, the script will attempt to use an existing credential associated to EndpointsCredentialFriendlyName.
	# If both this parameter and EndpointsCredential are passed in, a new credential by the name of EndpointsCredentialFriendlyName will be created.
	# If both this parameter and EndpointsCredential are passed in and a credential by the name of EndpointsCredentialFriendlyName already exists, an error will be returned. To overwrite the existing credential, apply the Force switch.
	# If neither this parameter nor EndpointsCredential are passed in, the Default Credential within Security Controls or session credential will be used if no Default is set.
	[Parameter(Mandatory = $false)]
	[String]$EndpointsCredentialFriendlyName,

	# The credential passed in for the endpoint machines. The type must be PSCredential.
	# If this parameter is passed in but EndpointsCredentialFriendlyName is not, the script will create a new credential with a generic friendly name "API Machine Group Endpoint Credential".
	# If both this parameter and EndpointsCredentialFriendlyName are passed in, a new credential by the name of EndpointsCredentialFriendlyName will be created.
	# If both this parameter and EndpointsCredentialFriendlyName are passed in and a credential by the name of EndpointsCredentialFriendlyName already exists, an error will be returned. To overwrite the existing credential, apply the Force switch.
	# If neither this parameter nor EndpointsCredential are passed in, the Default Credential within Security Controls or session credential will be used if no Default is set.
	[Parameter(Mandatory = $false)]
	[PSCredential]$EndpointsCredential,

	# The credential used to authenticate as an administrator with capabilities of decrypting the endpoint credentials.
	# This parameter will overwrite the existing session credential.
	# This credential is an administrator on the console machine. This credential is used to:
	# 1) Authenticate to the console machine REST API.
	# 2) Used as the session credential on the console.
	# 3) Used as the credential that starts the scan and deployments.
	[Parameter(Mandatory = $true)]
	[PSCredential]$SessionCredential,

	# The following will happen if force switch is used:
	# 1) If both EndpointsCredentialFriendlyName and EndpointsCredential are passed in, the script will overwrite the existing endpoint credential associated to EndpointsCredentialFriendlyName.
	# 2) If a scan template already exists that associates with ScanTemplateName, the scan template will be overwritten.
	[Parameter(Mandatory = $false)]
	[Switch]$Force,

	# Log file path. The default log file path will be the current file path location.
	[Parameter(Mandatory = $false)]
	[ValidateScript( { Test-Path $_ -PathType Leaf })]
	[String]$LogFilePath = '.\Invoke-ScanAndDeployPatchGroup.log'
)

Add-Type -AssemblyName System.Security

# Adds a session credential which is used to authenticate as an administrator with capabilities of decrypting the endpoint credentials.
# The existing session credential will be overwritten.
# This credential is an administrator on the console machine. This credential is used to:
# 1) Authenticate to the console machine REST API.
# 2) Used as the session credential on the console.
# 3) Used as the credential that starts the scan and deployments.
function Add-SessionCredential
{
	Param
	(
		[String]$consoleName,
		[PSCredential]$authenticationAndSessionCredential
	)

	$uri = "https://${consoleName}:3121/st/console/api/v1.0/sessioncredentials"

	try
	{
		Invoke-RestMethod -Uri $uri -Method 'DELETE' -Credential $authenticationAndSessionCredential -ContentType 'application/json'
	}
	catch [Exception]
	{
		$ex = $_.Exception
		if ($ex.Response.StatusCode -ne 404)
		{
			throw
		}
	}

	try
	{
		$credentialRequest = Create-CredentialRequest -friendlyName "unused" -userName "unused" -password $authenticationAndSessionCredential.Password -consoleName $consoleName -authenticationAndSessionCredential $authenticationAndSessionCredential
		$sessionCredentialBody = $credentialRequest.Password | ConvertTo-Json -Depth 99
		Invoke-RestMethod -Uri $uri -Method 'POST' -Body $sessionCredentialBody -Credential $authenticationAndSessionCredential -ContentType 'application/json'
	}
	catch [Exception]
	{
		$ex = $_.Exception
		if ($ex.Response.StatusCode -ne 409)
		{
			throw
		}
	}
}

# Adds/overwrites or searches for a credential in Security Controls. The credential can be found in the UI through Manage -> Credentials.
# The credential will be used to authenticate to your endpoints.
function Initialize-Credential
{
	Param
	(
		[String]$consoleName,
		[String]$friendlyName,
		[PSCredential]$credentialPassedInByUser,
		[PSCredential]$authenticationAndSessionCredential
	)

	if ([String]::IsNullOrEmpty($friendlyName))
	{
		$friendlyName = 'API Machine Group Endpoint Credential'
	}

	$existingCredential = Get-ExistingCredential -consoleName $consoleName -friendlyName $friendlyName -authenticationAndSessionCredential $authenticationAndSessionCredential
	if ($null -eq $credentialPassedInByUser)
	{
		return Get-ResultsFromSearch -existingCred $existingCredential
	}

	if ($null -eq $existingCredential)
	{
		Write-Information "Creating new credential with the given friendly name - ${friendlyName}."
	}
	else
	{
		if (-not $Force.IsPresent)
		{
			Write-Information "A new credential by the name of - ${friendlyName}, could not be created because a credential with that name already exists. "
			Write-Information "Either don't pass in an EndpointsCredential in order to reuse the existing credential or apply the Force switch to overwrite the existing credential."

			exit
		}

		Write-Information "Overwriting credential with name - ${friendlyName}."
		$uri = "https://${consoleName}:3121/st/console/api/v1.0/credentials/" + $existingCredential.Id
		Invoke-RestMethod -Uri $uri -Method 'DELETE' -Credential $authenticationAndSessionCredential -ContentType 'application/json'
	}

	$uri = "https://${consoleName}:3121/st/console/api/v1.0/credentials"
	$credentialBody = Create-CredentialRequest -friendlyName $friendlyName -userName $credentialPassedInByUser.UserName -password $credentialPassedInByUser.Password -consoleName $consoleName -authenticationAndSessionCredential $authenticationAndSessionCredential | ConvertTo-Json -Depth 99

	return Invoke-RestMethod -Uri $uri -Method 'POST' -Body $credentialBody -Credential $authenticationAndSessionCredential -ContentType 'application/json'
}

function Get-ExistingCredential
{
	Param
	(
		[String]$consoleName,
		[String]$friendlyName,
		[PSCredential]$authenticationAndSessionCredential
	)

	try
	{
		$uri = "https://${consoleName}:3121/st/console/api/v1.0/credentials?name=$friendlyName"
		$existingCredential = Invoke-RestMethod -Uri $uri -Method 'GET' -Credential $authenticationAndSessionCredential -ContentType 'application/json'
	}
	catch [Exception]
	{
		$ex = $_.Exception
		if ($ex.Response.StatusCode -eq 404)
		{
			$existingCredential = $null
		}
		else
		{
			throw
		}
	}

	return $existingCredential
}

function Get-ResultsFromSearch
{
	Param
	(
		[Object]$existingCredential
	)

	$searchFailed = $null -eq $existingCredential
	if ($searchFailed)
	{
		Write-Information "The credential with the given friendly name - ${friendlyName}, was not found."

		exit
	}

	Write-Information "An existing endpoint credential was found with the provided friendly name - ${friendlyName}."

	return $existingCredential
}

# Adds a patch scan template that only scans for the patches contained in the patch group associated to the script input param $PatchGroupId
function Add-PatchScanTemplate
{
	Param
	(
		[String]$patchGroupId,
		[String]$scanTemplateName,
		[String]$consoleName,
		[PSCredential]$authenticationAndSessionCredential
	)

	$uri = "https://${consoleName}:3121/st/console/api/v1.0/patch/scanTemplates?name=$scanTemplateName"
	$existingScanTemplate = Invoke-RestMethod -Uri $uri -Method 'GET' -Credential $authenticationAndSessionCredential -ContentType 'application/json'
	if ($existingScanTemplate.Count -gt 0)
	{
		if (-not $Force.IsPresent)
		{
			if ($existingScanTemplate.Value.PatchFilter.PatchGroupIds -notcontains $patchGroupId)
			{
				Write-Information "An existing patch scan template associated to the name - ${scanTemplateName}, was found but is not scanning for the patch group that is being used. Apply the Force switch to overwrite this scan template."

				exit
			}

			Write-Information "An existing patch scan template associated to the name - ${scanTemplateName}, was found and will be used."

			return $existingScanTemplate.Value
		}

		Write-Information "An existing patch scan template associated to the name - ${scanTemplateName}, was found and will be overwritten."
		$existingScanTemplateId = $existingScanTemplate.Value.Id
		$uri = "https://${consoleName}:3121/st/console/api/v1.0/patch/scanTemplates/$existingScanTemplateId"
		Invoke-RestMethod -Uri $uri -Method 'DELETE' -Credential $authenticationAndSessionCredential -ContentType 'application/json'
	}

	$scanTemplateBody = @{ name = $scanTemplateName; PatchFilter = @{ patchGroupFilterType = 'Scan'; patchGroupIds = @($patchGroupId) } } | ConvertTo-Json -Depth 99
	$uri = "https://${consoleName}:3121/st/console/api/v1.0/patch/scanTemplates"

	return Invoke-RestMethod -Uri $uri -Method 'POST' -Body $scanTemplateBody -Credential $authenticationAndSessionCredential -ContentType 'application/json'
}

# Creates and returns a secure credential body encrypted with AES algorithm and RSA certificate.
# The purpose of this is to prevent password strings from ever sitting on the garbage collected heap in powershell or .NET memory space.
# Easier solution if passwords sitting in the garbage collected heap is not a problem: $body = @{ "userName" = $userName; "name" = $friendlyName; "clearText" = "PasswordHere"; }
# Passwords are ALWAYS transmitted over a secure TLS channel regardless of the encryption chosen.
function Create-CredentialRequest
{
	param
	(
		[String]$friendlyName,
		[String]$userName,
		[SecureString]$password,
		[String]$consoleName,
		[PSCredential]$authenticationAndSessionCredential
	)
 
	$body = @{ "userName" = $userName; "name" = $friendlyName; }
	$bstr = [IntPtr]::Zero;
	try
	{
		# Create an AES Session key.
		$algorithm = 'Aes'
		$aes = [System.Security.Cryptography.SymmetricAlgorithm]::Create($algorithm);
		$keyBytes = $aes.Key;
 
		# Encrypt the session key with the console cert
		$encryptedKey = Encrypt-RSAConsoleCert -toEncrypt $keyBytes -authenticationAndSessionCredential $authenticationAndSessionCredential -consoleName $consoleName
		$session = @{ "algorithmIdentifier" = $algorithm; "encryptedKey" = [Convert]::ToBase64String($encryptedKey); "iv" = [Convert]::ToBase64String($aes.IV); }
 
		# Encrypt the password with the Session key.
		$cryptoTransform = $aes.CreateEncryptor();
 
		# Copy the BSTR contents to a byte array, excluding the trailing string terminator.
		$size = [System.Text.Encoding]::Unicode.GetMaxByteCount($password.Length - 1);
 
		$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)
		$clearTextPasswordArray = New-Object Byte[] $size
		[System.Runtime.InteropServices.Marshal]::Copy($bstr, $clearTextPasswordArray, 0, $size)
		$cipherText = $cryptoTransform.TransformFinalBlock($clearTextPasswordArray, 0 , $size)
 
		$passwordJson = @{ "cipherText" = $cipherText; "protectionMode" = "SessionKey"; "sessionKey" = $session }
	}
	finally
	{
		# Ensure All sensitive byte arrays are cleared and all crypto keys/handles are disposed.
		if ($null -ne $clearTextPasswordArray)
		{
			[Array]::Clear($clearTextPasswordArray, 0, $size)
		}
		if ($null -ne $keyBytes)
		{
			[Array]::Clear($keyBytes, 0, $keyBytes.Length)
		}
		if ($bstr -ne [IntPtr]::Zero)
		{
			[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
		}
		if ($null -ne $cryptoTransform)
		{
			$cryptoTransform.Dispose()
		}
			if ($null -ne $aes)
		{
			$aes.Dispose()
		}
	}

	$body.Add("password", $passwordJson)

	return $body
}

# The encryption process used within the function 'Create-CredentialRequest'. Encrypted with protects console certificate.
# The encrypted rsa public key is returned.
function Encrypt-RSAConsoleCert
{
	param
	(
		[Byte[]]$toEncrypt,
		[String]$consoleName,
		[PSCredential]$authenticationAndSessionCredential
	)
	try
	{
		$certResponse = Invoke-RestMethod "https://${consoleName}:3121/st/console/api/v1.0/configuration/certificate" -Method GET -Credential $authenticationAndSessionCredential -Verbose
		[Byte[]] $rawBytes = ([Convert]::FromBase64String($certResponse.derEncoded))
		$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @(, $rawBytes)
		$rsaPublicKey = $cert.PublicKey.Key;
		$encryptedKey = $rsaPublicKey.Encrypt($toEncrypt, $true);

		return $encryptedKey
	}
	catch [Exception]
	{
		$ex = $_.Exception
		if ($ex.Response.StatusCode -eq 401)
		{
			if ($null -ne $cert)
			{
				$cert.Dispose()
			}

			Write-Error "The session credential was invalid."

			exit
		}
		else
		{
			throw
		}
	}
	finally
	{
		if ($null -ne $cert)
		{
			$cert.Dispose()
		}
	}
}

# Performs a scan with the scan template associated to the scan template ID input param.
# Waits for the scan to complete and then returns the scan ID.
function Invoke-Scan
{
	param
	(
		[String]$scanName,
		[String]$scanTemplateId,
		[String[]]$endpointMachineNames,
		[String]$endpointCredentialId,
		[String]$consoleName,
		[PSCredential]$authenticationAndSessionCredential
	)

	$scanBody = @{ Name = $scanName; TemplateId = $scanTemplateId; EndpointNames = $endpointMachineNames; CredentialId = $endpointCredentialId; } | ConvertTo-Json -Depth 99
	$uri = "https://${consoleName}:3121/st/console/api/v1.0/patch/scans"
	$scanOperation = Invoke-WebRequest -Uri $uri -Method 'POST' -Body $scanBody -Credential $authenticationAndSessionCredential -Verbose -ContentType 'application/json' -UseBasicParsing
 
	# Wait for scan to complete
	$completedScan = Wait-Operation -operationLocation $scanOperation.headers['Operation-Location'] -timeoutMinutes 5 -authenticationAndSessionCredential $authenticationAndSessionCredential
 
	# Get the scan id for future use
	$scan = Invoke-RestMethod -Uri $completedScan.resourceLocation -Credential $authenticationAndSessionCredential -Verbose -Method GET
	Write-Information ( "Scan complete " + $scan.id)

	return $scan.id
}

# Part of the 'Invoke-Scan' function.
# Waits for scan to complete.
function Wait-Operation
{
	param
	(
		[String]$operationLocation,
		[Int32]$timeoutMinutes,
		[PSCredential]$authenticationAndSessionCredential
	)
 
	$startTime = [DateTime]::Now
	$operationResult = Invoke-RestMethod -Uri $operationLocation -Method Get -Credential $authenticationAndSessionCredential -Verbose
	while ($operationResult.Status -eq 'Running')
	{
		if ([DateTime]::Now -gt $startTime.AddMinutes($timeoutMinutes))
		{
			throw "Timed out waiting for operation to complete"
		}
 
		Start-Sleep 10
		$operationResult = Invoke-RestMethod -Uri $operationLocation -Method GET -Credential $authenticationAndSessionCredential -Verbose
	}
 
	return $operationResult
}

# Deploys the patches from the scan result.
# This function is only run if -DeployPatches switch is used.
function Invoke-Deploy
{
	Param
	(
		[String]$scanId,
		[String]$deploymentTemplateId,
		[String]$consoleName,
		[PSCredential]$authenticationAndSessionCredential
	)

	Write-Information "Starting deployment"
	$deployBody = @{ ScanId = $scanId; TemplateId = $deploymentTemplateId } | ConvertTo-Json -Depth 99
	$uri = "https://${consoleName}:3121/st/console/api/v1.0/patch/deployments"
	$deploy = Invoke-WebRequest -Uri $uri -Method 'POST' -Body $deployBody -Credential $authenticationAndSessionCredential -Verbose -ContentType 'application/json' -UseBasicParsing

	# Wait until deployment has a deployment resource location
	$operationUri = $deploy.Headers['Operation-Location']
	$operation = Invoke-RestMethod -Uri $operationUri -Credential $authenticationAndSessionCredential -Verbose -Method GET

	while ((($null -eq $operation.resourceLocation) -or ($operation.operation -eq "PatchDownload")) -and -not ($operation.status -eq "Succeeded"))
	{
		if (($operation.operation -eq "PatchDownload") -and ($null -ne $operation.percentComplete))
		{
			Write-Information ("Downloading patches..." + $operation.percentComplete + "%")
		}

		Start-Sleep -Seconds 5

		$operation = Invoke-RestMethod -Uri $operationUri -Credential $authenticationAndSessionCredential -Verbose -Method GET
	}

	# It's possible we didn't have anything to patch in which case we're already succeeded.
	# If so, don't bother getting machine statuses as it will never return anything good.
	if ($operation.status -ne "Succeeded")
	{
		Write-Information "Deployment scheduled"

		# Start getting deployment detailed status updates
		$statusUri = $deploy.Headers['Location'] + '/machines'
		$machineStatuses = Invoke-RestMethod $statusUri -Credential $authenticationAndSessionCredential -Verbose -Method GET

		# Now start getting and displaying the statuses
		while (($machineStatuses.value[0].overallState -ne "Complete") -and ($machineStatuses.value[0].overallState -ne "Failed"))
		{
			Write-Information ("Overall Status = " + $machineStatuses.value[0].overallState)
			Write-Information ("Status Description = " + $machineStatuses.value[0].statusDescription)

			# Only check for new updates every 30 seconds
			Start-Sleep -Seconds 30

			$machineStatuses = Invoke-RestMethod $statusUri -Credential $authenticationAndSessionCredential -Verbose -Method GET
		}
	}
}

###################################################################################
# Start Script
# This script works well in union with the 'Invoke-CvesToPatchGroup.ps1' script. Check out 'Invoke-ScanAndDeployCveDocument.ps1' to see 'Invoke-CveToPatchGroup.ps1' and the current script in use.
###################################################################################

Start-Transcript -Append "$LogFilePath" | Out-Null
try
{
	Add-SessionCredential -authenticationAndSessionCredential $SessionCredential -consoleName $ConsoleName
	if ($null -ne $EndpointsCredential -or -not [String]::IsNullOrEmpty($EndpointsCredentialFriendlyName))
	{
		$endpointCredential = Initialize-Credential -consoleName $ConsoleName -friendlyName $EndpointsCredentialFriendlyName -credential $EndpointsCredential -authenticationAndSessionCredential $SessionCredential
	}

	$scanTemplate = Add-PatchScanTemplate -patchGroupId $PatchGroupId -scanTemplateName $ScanTemplateName -consoleName $ConsoleName -authenticationAndSessionCredential $SessionCredential
	$scanId = Invoke-Scan -scanName $ScanTemplateName -scanTemplateId $scanTemplate.id -endpointMachineNames $EndpointMachineNames -endpointCredentialId $endpointCredential.Id -consoleName $ConsoleName -authenticationAndSessionCredential $SessionCredential
	if (-not [String]::IsNullOrEmpty($DeploymentTemplateId))
	{
		Invoke-Deploy -scanId $scanId -deploymentTemplateId $DeploymentTemplateId -consoleName $ConsoleName -authenticationAndSessionCredential $SessionCredential
	}
}
catch [Exception]
{
	$private:e = $_.Exception
	do
	{
		Write-Error -Exception $private:e
		$private:e = $private:e.InnerException
	}
	while ($null -ne $private:e)
}
finally
{
	Stop-Transcript | Out-Null
}