PowerShell Example: Automate to Compliance

Goal

Provide an example PowerShell script that scans Windows target machines and deploys all missing patches.

Overview

The script will automatically repeat the following steps until no more patches are detected as missing and your machines are in full compliance.

Product levels are not supported by this script.

  1. Scan for patches using the specified patch scan template.
  2. Attempt to deploy all missing patches according to the specified patch deployment template.
  3. Reboot as directed.

The script is run from a PowerShell command prompt such as PowerShell ISE or a similar tool with Run as Administrator privileges. It can be run from the Security Controls console or remotely from a server or workstation. If the script is run remotely, proper credentials must be provided.

See the Description section of the script for examples of how to execute the script and to view a list of the parameters that are available.

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
	Iteratively scan and deploy to target Windows machine(s) until no patches are detected as 'Missing'.

.DESCRIPTION
	Iteratively scan and deploy to target Windows machine(s) until no patches are detected as 'Missing'.

	This script assumes that it will be running in the context of an administrative user on the same system
	as security controls. To run remotely (on a system other than the console) requires the additional
	configuration of the issuing cert of the console in the trusted root store along with supplying a session credential.
	See https://help.ivanti.com/iv/help/en_US/isec/API/Topics/Setup%20Script.htm for more info.

	It also assumes that connection credentials have been set for the machine or at the machine group level.

.PARAMETER MachineGroup
	Machine group to be patched.

.PARAMETER MachineName
	Machine name to be patched.

.PARAMETER ConsoleName
	Name of console.

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

.PARAMETER ScanTemplateName
	Scan template. Windows only. Default to Security Patch Scan.

.PARAMETER DeploymentTemplateName
	Deployment template. Windows only. Default to Standard.

.PARAMETER AttemptLimit
	Max attempts. Default to 5.

.PARAMETER TimeoutLimitInMinutes
	Operation timeout limit to wait for a single scan/deployment in minutes. Default to 180 minutes.

.PARAMETER Credential
	Credential used to connect to console REST Api. If omitted, will fall back to UseDefaultCredentials.

.PARAMETER LogFileFolder
	Log file folder.

.PARAMETER OperationName
	Name used for the scan, deployment and log file.

.EXAMPLE
	Running the script on a Console
	.\Install-AllMissingPatches.ps1 -MachineGroup "windows-machines"

.EXAMPLE
	Running the script on a Console using a custom scan template
	.\Install-AllMissingPatches.ps1 -MachineGroup "windows-machines" -ScanTemplateName "sql-server-scan-template"

.EXAMPLE
	Running the script on an Agent/Client (Credential is required and the issuing cert of the console must be in the trusted root store,
	see https://help.ivanti.com/iv/help/en_US/isec/API/Topics/Setup%20Script.htm for more info.)
	$Credential=Get-Credential
	.\Install-AllMissingPatches.ps1 -MachineGroup "windows-machines" -ConsoleName "console" -Credential $Credential
#>

[CmdletBinding(DefaultParameterSetName = 'MachineGroup')]
param (   
    
	[Parameter(Mandatory=$true, ParameterSetName="MachineGroup", HelpMessage="Machine group to be patched.")]
	[String] $MachineGroupName,

	[Parameter(Mandatory=$true, ParameterSetName="MachineName", HelpMessage="Machine name to be patched.")]
	[String] $MachineName,

	[Parameter(Mandatory=$false, HelpMessage="Name of console. Default to current machine. ")]
	[String] $ConsoleName=$null,

	[Parameter(Mandatory=$false, HelpMessage="Port used for Security Controls REST API calls. Default is 3121.")]
	[ValidateRange(0, 65353)]
	[UInt32] $Port=3121,

	[Parameter(Mandatory=$false, HelpMessage="Scan template. Default to Security Patch Scan.")]
	[String] $ScanTemplateName="Security Patch Scan",

	[Parameter(Mandatory=$false, HelpMessage="Deployment template. Default to Standard.")]
	[String] $DeploymentTemplateName="Standard",

	[Parameter(Mandatory=$false, HelpMessage="Max attempts. Default to 5.")]
	[UInt32] $DeployAttemptLimit=5,

	[Parameter(Mandatory=$false, HelpMessage="Operation timeout limit to wait for a single scan/deployment in minutes. Default to 180 minutes.")]
	[ValidateRange(5, 1440)]
	[UInt32] $TimeoutLimitInMinutes=180,

	[Parameter(Mandatory = $false, HelpMessage="Credential used to access remote machine. If omitted, will fall back to UseDefaultCredentials.")]
	[PSCredential] $Credential,

	[Parameter(Mandatory = $false, HelpMessage="Log file folder.")]
	[ValidateScript( { Test-Path -Path $_ })] 
	[String]$LogFileFolder,

	[Parameter(Mandatory=$false, HelpMessage="Name used for the scan, deployment and log file.")]
	[String] $OperationName

)

#Helper function to write logs where needed
function Log-Debug
{
	param
	(
		[string] $LogPhrase
	)
	#Add timestamp to beginning of any string
	filter timestamp {"$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") - $_"}
	Write-Debug ($LogPhrase | timestamp)   
}

function Log-Host
{
	param
	(
		[string] $LogPhrase
	)
	#Add timestamp to beginning of any string
	filter timestamp {"$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") - $_"}
	Write-Host ($LogPhrase | timestamp)   
}

Add-Type -AssemblyName "System.Security" | Out-Null

# 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,
		[Int32]$port,
		[PSCredential]$authenticationAndSessionCredential
	)

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

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

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

# 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,
		[Int32]$port,
		[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 -port $port
		$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)
 
		$serializedPossword = @{ "cipherText" = $cipherText; "protectionMode" = "SessionKey"; "sessionKey" = $session }
	}
	finally
	{
		# Ensure All sensitive byte arrays are cleared and all crypto keys/handles are disposed.
		if ($clearTextPasswordArray -ne $null)
		{
			[Array]::Clear($clearTextPasswordArray, 0, $size)
		}
		if ($keyBytes -ne $null)
		{
			[Array]::Clear($keyBytes, 0, $keyBytes.Length)
		}
		if ($bstr -ne [IntPtr]::Zero)
		{
			[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
		}
		if ($cryptoTransform -ne $null)
		{
			$cryptoTransform.Dispose()
		}
		if ($aes -ne $null)
		{
			$aes.Dispose()
		}
	}

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

	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,
		[Int32]$port,
		[PSCredential]$authenticationAndSessionCredential
	)
	try
	{
		$certResponse = Invoke-RestMethod $Uris.CertificateConsole -Method GET -Credential $authenticationAndSessionCredential
		[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 ($cert -ne $null)
			{
				$cert.Dispose()
			}

			Write-Error "The session credential was invalid."

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

#loops through a Get request when multiple pages exist and returns list. Will stop when requested count is met.
function Get-PaginatedResults
{
	param
	(
		[String]$uri,
		[Int]$maxcount
	)

	$entireList = [System.Collections.ArrayList]@()
	$nextUri = $uri
	do
	{
		Log-Debug "Get-PaginatedResults uri: $nextUri"
		Log-Debug "Get-PaginatedResults maxcount: $maxcount"

		if ($Credential -ne $null)
		{
			$result = Invoke-RestMethod -Method Get -Uri $nextUri -Credential $Credential
		}
		else
		{
			$result = Invoke-RestMethod -Method Get -Uri $nextUri -UseDefaultCredentials
		}
        
		$result.value | Foreach-Object { $entireList.Add($_) }

		$nextUri = $result.links.next.href
        
		if ($maxcount) 
		{
			if($entireList.length -ge $maxcount) 
			{
				return $entireList
			}
		}

	} until ($null -eq $nextUri)
    
	return $entireList
}

#will wait until an operation is marked as not "Running"
function Wait-Operation
{
	param(
		[String] $operationLocation,
		[Int32] $timeoutMinutes
	)

	Log-Debug "Run Wait-Operation with operationLocation=$operationLocation and timeoutMinutes=$timeoutMinutes"

	$startTime = [DateTime]::UtcNow
	if ($Credential -ne $null)
	{
		$operationResult = Invoke-RestMethod -Uri $operationLocation -Method Get -Credential $Credential
	}    
	else
	{
		$operationResult = Invoke-RestMethod -Uri $operationLocation -Method Get -UseDefaultCredentials
	}
    
	#If operation is downloading patches, loop until operation moves to deployment
	if ($operationResult.operation -eq "PatchDownload")
	{
		Log-Debug "Downloading Patches"
		while ($operationResult.operation -eq "PatchDownload")
		{
			if ([DateTime]::UtcNow -gt $startTime.AddMinutes($timeoutMinutes))
			{
				Log-Debug "The operation timed out after $($timeoutMinutes) minutes."
				return
			}
			Start-Sleep 5
			if ($Credential -ne $null)
			{
				$operationResult = Invoke-RestMethod -Uri $operationLocation -Method Get -Credential $Credential
			}
			else
			{
				$operationResult = Invoke-RestMethod -Uri $operationLocation -Method Get -UseDefaultCredentials
			}
            
			Log-Debug "Operation Result: $operationResult"
		}
	}
	# Loop on child of operation until isComplete is True
	if ($operationResult.operation -eq "PatchDeployment")
	{
		Log-Debug "Beginning Deployment"

		if ([String]::IsNullOrWhiteSpace($operationResult.resourceLocation))
		{
			Log-Debug "Invalid ResourceLocation, Abort Deployment"
			return
		}

		if ($Credential -ne $null)
		{
			$deploymentstatus = Invoke-RestMethod -Uri $operationResult.resourceLocation -Credential $Credential
		}
		else
		{
			$deploymentstatus = Invoke-RestMethod -Uri $operationResult.resourceLocation -UseDefaultCredentials
		}
        
		Log-Debug "Deployment Status: $deploymentstatus"
		while ($deploymentstatus.isComplete -eq $False)
		{
			if ([DateTime]::UtcNow -gt $startTime.AddMinutes($timeoutMinutes))
			{
				Log-Debug "The operation timed out after $($timeoutMinutes) minutes."
				return
			}
			Start-Sleep 5
			if ($Credential -ne $null)
			{
				$operationResult = Invoke-RestMethod -Uri $operationLocation -Method Get -Credential $Credential
			}
			else
			{
				$operationResult = Invoke-RestMethod -Uri $operationLocation -Method Get -UseDefaultCredentials
			}            

			Log-Debug "Operation Result Status: $($operationResult.Status)"
			if ($Credential -ne $null)
			{
				$deploymentstatus = Invoke-RestMethod -Uri $operationResult.resourceLocation -Credential $Credential
			}
			else
			{
				$deploymentstatus = Invoke-RestMethod -Uri $operationResult.resourceLocation -UseDefaultCredentials
			}
            
			Log-Debug "Deployment Status: $deploymentstatus"
		}

	}
	# If operation is not downloading, or deploying, default to loop on operation "Running"
	while ($operationResult.Status -eq 'Running')
	{
		if ([DateTime]::UtcNow -gt $startTime.AddMinutes($timeoutMinutes))
		{
			Log-Debug "The operation timed out after $timeoutMinutes minutes."
			return
		}
		Start-Sleep 5
		if ($Credential -ne $null)
		{
			$operationResult = Invoke-RestMethod -Uri $operationLocation -Method Get -Credential $Credential
		}
		else
		{
			$operationResult = Invoke-RestMethod -Uri $operationLocation -Method Get -UseDefaultCredentials
		}
        
		Log-Debug "Operation Result: $($operationResult.Status)"
	}
	#Write out the full operation status after completion
	Log-Debug "Final Operation Result: $($operationResult.Status)"
	return $operationResult
}

#Kick off scan with a machine group, patch template
function Scan-Operation
{
	param(
		[Parameter(Mandatory=$true,
		ParameterSetName="machineGroupId")]
		[Int32[]]
		$machineGroupId,

		[Parameter(Mandatory=$true,
		ParameterSetName="machineName")]
		[String[]]
		$scanMachineName,        

		[Guid] $templateId
	)

	if ($machineGroupId) 
	{
		$body = @{ machineGroupIds = @( $machineGroupId );
			Name = $OperationName;
			TemplateId = $templateId;
		} | ConvertTo-Json -Depth 99
	}

	if ($scanMachineName) 
	{
		$body = @{ endpointNames = @( $scanMachineName );
			Name = $OperationName;
			TemplateId = $templateId;
		} | ConvertTo-Json -Depth 99
	}

	if ($Credential -ne $null)
	{
		$scanOperation = Invoke-WebRequest -Uri $Uris.PatchScans -Method Post -Body $body -ContentType 'application/json' -UseBasicParsing -Credential $Credential
	}
	else
	{
		$scanOperation = Invoke-WebRequest -Uri $Uris.PatchScans -Method Post -Body $body -ContentType 'application/json' -UseBasicParsing -UseDefaultCredentials
	}
	Wait-Operation $scanOperation.headers['Operation-Location'] $TimeoutLimitInMinutes | Out-Null
	Log-Debug $scanOperation
	$scan = (ConvertFrom-Json $([String]::new($scanOperation.Content)))

	if ($Credential -ne $null)
	{
		$foundScan = Invoke-RestMethod -Uri $scan.links.self.href -Credential $Credential
	}
	else
	{
		$foundScan = Invoke-RestMethod -Uri $scan.links.self.href -UseDefaultCredentials
	}
	Log-Debug $foundScan
	return $foundScan
}

#Kick off deployment with a scan result and deployment template id
function Deployment-Operation
{
	param(
		[Guid] $scanId,
		[Guid] $deploymentTemplateId
	)
	$body = @{ scanID = $scanId; templateID = $deploymentTemplateId } | ConvertTo-Json -Depth 99
	if ($Credential -ne $null)
	{
		$deploymentOperation = Invoke-WebRequest -Uri $Uris.PatchDeployments -Method Post -Body $body -ContentType 'application/json' -UseBasicParsing -Credential $Credential
	}
	else
	{
		$deploymentOperation = Invoke-WebRequest -Uri $Uris.PatchDeployments -Method Post -Body $body -ContentType 'application/json' -UseBasicParsing -UseDefaultCredentials
	}
	Log-Debug "$($deploymentOperation.headers['Operation-Location'])"
	Wait-Operation $deploymentOperation.headers['Operation-Location'] $TimeoutLimitInMinutes | Out-Null
	if ($Credential -ne $null)
	{
		$foundDeployment = Invoke-RestMethod -Uri $deploymentoperation.headers.'Operation-Location' -Method Get -Credential $Credential
	}
	else
	{
		$foundDeployment = Invoke-RestMethod -Uri $deploymentoperation.headers.'Operation-Location' -Method Get -UseDefaultCredentials
	}    
	return $foundDeployment
}

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

#Get time to return full elapsed time
$startTime = (Get-Date)

if ($ConsoleName -eq $null -or $ConsoleName -eq "")
{
	$ConsoleName = $env:computername
	if ($env:userdnsdomain -ne $null)
	{
	#It may not work for console disconnected from domain
		#$ConsoleName = "$ConsoleName.$env:userdnsdomain"
	}
}

if ([String]::IsNullOrWhiteSpace($OperationName))
{
	if ($MachineGroupName)
	{
		$OperationName = "Install-AllMissingPatches-$MachineGroupName"
	}
	else
	{
		$OperationName = "Install-AllMissingPatches-$MachineName"
	}
}

#The variables set above are passed into the URIs below to set each unique API URI
$Uris =
@{
	Credentials = "https://$ConsoleName`:$Port/st/console/api/v1.0/credentials"
	CertificateConsole = "https://$ConsoleName`:$Port/st/console/api/v1.0/configuration/certificate"
	MachineGroups = "https://$ConsoleName`:$Port/st/console/api/v1.0/machinegroups"
	Operations = "https://$ConsoleName`:$Port/st/console/api/v1.0/operations"
	PatchDeployments = "https://$ConsoleName`:$Port/st/console/api/v1.0/patch/deployments"
	PatchDeployTemplates = "https://$ConsoleName`:$Port/st/console/api/v1.0/patch/deploytemplates"
	PatchDownloads = "https://$ConsoleName`:$Port/st/console/api/v1.0/patch/downloads"
	PatchDownloadsScansPatch = "https://$ConsoleName`:$Port/st/console/api/v1.0/patch/downloads/scans"
	PatchGroups = "https://$ConsoleName`:$Port/st/console/api/v1.0/patch/groups"
	PatchScans = "https://$ConsoleName`:$Port/st/console/api/v1.0/patch/scans"
	PatchScanMachines = "https://$ConsoleName`:$Port/st/console/api/v1.0/patch/scans/{0}/machines"
	PatchScanMachinesPatches = "https://$ConsoleName`:$Port/st/console/api/v1.0/patch/scans/{0}/machines/{1}/patches"
	PatchScanTemplates = "https://$ConsoleName`:$Port/st/console/api/v1.0/patch/scanTemplates"
}

if ($LogFileFolder)
{
	Start-Transcript -Path "$LogFileFolder\$OperationName.log" -Append -Force
}

try
{
	if ($Credential -ne $null)
	{
		Add-SessionCredential -authenticationAndSessionCredential $Credential -consoleName $ConsoleName -port $Port
	}

	#Set TLS protocol to TLS1.2
	[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType] 'Tls12'

	#Find all objects that match each name
	if (($_foundScanTemplate = Get-PaginatedResults $Uris.PatchScanTemplates | Where-Object { $_.Name -eq $ScanTemplateName }) -eq $null) { throw "No patch template found with specified name." }
	if (($_foundDeploymentTemplate = Get-PaginatedResults $Uris.PatchDeployTemplates | Where-Object { $_.Name -eq $DeploymentTemplateName }) -eq $null) { throw "No deployment template found with specified name." }
	if ($MachineGroupName) { if (($_foundMachineGroup = Get-PaginatedResults $Uris.MachineGroups | Where-Object { $_.Name -eq $MachineGroupName }) -eq $null) { throw "No machine group found with specified name." } }

	#Run initial scan
	Log-Host "Starting Scan..."
	if ($MachineGroupName) 
	{
		$scanResult = Scan-Operation -machineGroupId $_foundMachineGroup.id -templateId $_foundScanTemplate.id
	}
	else 
	{
		$scanResult = Scan-Operation -scanMachineName $MachineName -templateId $_foundScanTemplate.id
	}

	if ($scanResult.links.machines.href)
	{
		$machinestatus = Get-PaginatedResults $scanResult.links.machines.href
		$patchCount = ($machinestatus.missingpatchcount | Measure-Object -sum).sum
		$failedMachines = (($machinestatus | Where-Object {$null -ne $_.name} | Where-Object { $_.errorNumber -ne 0 }) | Measure-Object).count
	}
	else
	{
		$patchCount = 0
		$failedMachines = 0
	}

	$attempts = 0

	$scannedMachines = (($machinestatus | Where-Object {$null -ne $_.name} | Where-Object { $_.errorNumber -eq 0 }) | Measure-Object).count

	if ($scannedMachines -ne 0)
	{
		Log-Host "$patchCount Total Patches Missing"
	}

	Log-Host "$failedMachines Machines Failed Scan"

	if ($scannedMachines -eq 0)
	{
		#Nothing to do
	}
	elseif ($patchcount -eq 0)
	{
		Log-Host "No Missing Patches"
	}
	else
	{
		do 
		{
			if ($attempts -eq $DeployAttemptLimit)
			{
				Log-Host "Exceeded Maximum Number of Deployment Attempts"
				$endTime = (Get-Date)
				$elapsedTime = $endTime-$startTime
				$formatTime = 'Duration: {0:mm} min {0:ss} sec' -f $elapsedTime
				Log-Host "$formatTime"
				return
			}
        
			$attempts++
			Log-Host "Deployment Attempt#: $attempts of $DeployAttemptLimit"
        
			Log-Host "Starting Deployment..."
			Deployment-Operation $scanResult.id $_foundDeploymentTemplate.id | Out-Null
        
			Log-Host "Starting Scan..."

			if ($MachineGroupName) 
			{
				$scanResult = Scan-Operation -machineGroupId $_foundMachineGroup.id -templateId $_foundScanTemplate.id
			}
			else 
			{
				$scanResult = Scan-Operation -scanMachineName $MachineName -templateId $_foundScanTemplate.id
			}

			if ($scanResult.links.machines.href)
			{
				$machinestatus = Get-PaginatedResults $scanResult.links.machines.href
				$patchCount = ($machinestatus.missingpatchcount | Measure-Object -sum).sum
				$failedMachines = (($machinestatus | Where-Object {$null -ne $_.name} | Where-Object { $_.errorNumber -ne 0 }) | Measure-Object).count
				$scannedMachines = (($machinestatus | Where-Object {$null -ne $_.name} | Where-Object { $_.errorNumber -eq 0 }) | Measure-Object).count
			}
			else
			{
				$patchCount = 0
				$failedMachines = 0
			}

			if ($scannedMachines -ne 0)
			{
				Log-Host "$patchCount Total Patches Missing"
			}

			Log-Host "$failedMachines Machines Failed Scan"
		}
		until($patchCount -eq 0)
    
		if (($patchcount -eq 0) -and ($scannedMachines -ne 0))
		{
			Log-Host "No Missing Patches After $attempts Attempts"
		}
	}

	$endTime = (Get-Date)

	$elapsedTime = $endTime-$startTime
	$formatTime = 'Duration:{0:hh} hr {0:mm} min {0:ss} sec' -f $elapsedTime
	Log-Host "$formatTime"
}
catch
{
	#all error end up here, if they are breaking errors
	Throw $_
}
finally
{
	if ($EnableLogging)
	{
		Stop-Transcript
	}
}
##########################################################################
#            End Script
##########################################################################