CT-PS-Standard.psm1

<#
--------------------------------------------------------------------------------------------------
 
This is a standard module with a set of standard functions used across multiple scripts within CT.
 
Any changes to this module need to be published using the powershell script "Build_CT_Module.ps1"
 
--------------------------------------------------------------------------------------------------
HOW TO IMPORT INTO SCRIPT:
--------------------------------------------------------------------------------------------------
This module should be imported using the commands below (do not copy the asterix's, just whats between).
This will import this module AND initialise the script with all the standard features required by scripts,
including all the log files for each transaction
*****************
 
Install-Module -Name "CT-PS-Standard" -Force -AllowClobber -Repository PSGallery -Verbose:$VerbosePreference -ErrorAction Stop
Import-Module -Name "CT-PS-Standard" -Force -Verbose:$VerbosePreference -ErrorAction Stop
 
*****************
 
If this does not work or fails (often because the "basepowershellsetup.ps1 script hasn't run yet on the machine"), use the code below. Sometimes if the standard module has been imported globally and not released,
it can create locking issues, so it needs removal first.
*****************
 
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
if ($null -eq (Get-PSRepository)) {
    Register-PSRepository -Default -Verbose:$VerbosePreference
}
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.208 -Force -ErrorAction SilentlyContinue -Verbose:$VerbosePreference
Remove-Module -Name "CT-PS-Standard" -Force -Verbose:$VerbosePreference -ErrorAction SilentlyContinue
Uninstall-Module -Name "CT-PS-Standard" -AllVersions -Force -ErrorAction SilentlyContinue -Verbose:$VerbosePreference
Install-Module -Name "CT-PS-Standard" -Force -AllowClobber -Repository PSGallery -Verbose:$VerbosePreference -ErrorAction Stop
Import-Module -Name "CT-PS-Standard" -Force -Verbose:$VerbosePreference -ErrorAction Stop
 
*****************
 
--------------------------------------------------------------------------------------------------
 
 
--------------------------------------------------------------------------------------------------
LOG FILES
--------------------------------------------------------------------------------------------------
There are four log files initialised by this module that can be used for output.
You can write to each of these logs accordingly.
 
$Output_Log: This is the standard console output.
$Transcript_log: This is where the transcript is written to. You will need to start and stop the transcript inside your script by using the command "Start-Transcript -Path $Transcript_log -append | Out-Null"
$API_log: This is where the output from API posts should be sent.
$Install_log: This is where output from MSIEXEC commands should be logged
 
--------------------------------------------------------------------------------------------------
 
 
--------------------------------------------------------------------------------------------------
#>







# --------------------------------------------------------------------------------------------------
# The following commands will run when the module is imported
    #Begin {
    # Display some troubleshoot info

    <#
    Write-verbose "MyInvocation Info:
    $($MyInvocation | Select-Object -Property * | Out-String)
    BoundParameters:
    $($MyInvocation.BoundParameters | Select-Object -Property * | Out-String)
    UnboundArguments:
    $($MyInvocation.UnboundArguments | Select-Object -Property * | Out-String)
     
    "
 
    Write-verbose "Global MyInvocation Info:
    $($global:MyInvocation | Select-Object -Property * | Out-String)
    BoundParameters:
    $($global:MyInvocation.BoundParameters | Select-Object -Property * | Out-String)
    UnboundArguments:
    $($global:MyInvocation.UnboundArguments | Select-Object -Property * | Out-String)
 
    "
 
    Write-verbose "Current automatic variables:
     
    ScriptName: $($ScriptName)
    PSScriptRoot: $($PSScriptRoot)
    PSCommandPath: $($PSCommandPath)
 
    "
 
    Write-verbose "Current global variables:
     
    ScriptName: $($global:ScriptName)
    PSScriptRoot: $($global:PSScriptRoot)
    PSCommandPath: $($global:PSCommandPath)
     
    "
    #>


    $global:ErrorActionPreference = "Stop"
    $global:CT_DEST="C:\CT"  # Where the files are downloaded to
    $global:DateStamp = get-date -Format yyyyMMddTHHmmss # A formatted date strong

    try {
        $global:Script_Path = Split-Path $global:PSCommandPath -Parent

        if ($global:PSCommandPath -gt 4) {
            try {
                $Global:Script_Name = Split-Path $global:PSCommandPath -Leaf
                $Global:Script_Name = ($Global:Script_Name).Substring(0,($Global:Script_Name).Length-4)
            } catch {
                $Global:Script_Name = "Other_$(get-date -Format yyyyMMdd)"
            }
        } else {
            $Global:Script_Name = "Terminal_$(get-date -Format yyyyMMdd)"
        }
    } catch {
        $global:Script_Path = "C:\CT"
        $Global:Script_Name = "Terminal_$(get-date -Format yyyyMMdd)"
    }


    $global:Output_log = "$CT_DEST\logs\$($Script_Name)\$($DateStamp)_output.log"  # The output
    $global:Transcript_log = "$CT_DEST\logs\$($Script_Name)\$($DateStamp)_transcript.log"  # The powershell transcript file
    $global:API_log = "$($CT_DEST)\logs\$($Script_Name)\$($DateStamp)_API.log"
    $global:Install_log = "$CT_DEST\logs\$($Script_Name)\$($DateStamp)_install.log"  # The powershell installation file

    #} #End begin block

    
    #Process {

        
    # ComputerType will report if the machine is a workstation, DC, or non-DC server
    # 1 for workstations, 2 for DCs, and 3 for non-DC servers
    try {
        $global:ComputerType = (Get-CimInstance -ClassName Win32_OperatingSystem -Debug:$DebugPreference).ProductType
    } catch {
        write-host "There is a problem with this computer and updates are required for this script to continue."
        $line = $_.InvocationInfo.ScriptLineNumber
        write-Error "ERROR: [ML$($line)] There is a problem with this computer and updates are required for this script to continue.
        $($_)"
 -ErrorId "1001" -Category ObjectNotFound -CategoryReason "Cannot extract computer type from WMI Win32_OperatingSystem." -ErrorAction Continue
        #Stop-Transcript | Out-Null
        $exiterror = 1001
        throw "There is a problem with this computer and updates are required for this script to continue."
        exit $exiterror
    }



    try{

        # Check for a CT folder on the C: and if not, create it, however that location should already exist as part of the Start-Transcript command.
        if(-not( Test-Path -Path $CT_DEST )) {
            try{
                mkdir $CT_DEST > $null
                #Transcript-Log "New folder created at $CT_DEST."
            }catch{
                #Can't create the folder, therefore cannot continue
                $line = $_.InvocationInfo.ScriptLineNumber
                write-Error "ERROR: [ML$($line)] Cannot create folder $CT_DEST. $($_)" -Category WriteError -CategoryReason "Cannot create folder $($CT_DEST)." -ErrorId "1002" -ErrorAction Continue
                Write-Error $_ -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) -ErrorAction Continue
                #Stop-Transcript
                $exiterror = 1002
                exit $exiterror
                throw
            }
        }

        if(-not( Test-Path -Path "$($CT_DEST)\logs" )) {
            try{
                mkdir "$($CT_DEST)\logs" > $null
                #Transcript-Log "New logs folder created at $CT_DEST."
            }catch{
                #Can't create the folder, therefore cannot continue
                $line = $_.InvocationInfo.ScriptLineNumber
                write-Error "ERROR: [ML$($line)] Cannot create logs folder in $CT_DEST. $($_)" -Category WriteError -CategoryReason "Cannot create logs folder in $($CT_DEST)." -ErrorId "1003" -ErrorAction Continue
                Write-Error $_ -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) -ErrorAction Continue
                #Stop-Transcript
                $exiterror = 1003
                exit $exiterror
                throw
            }
        }

        if(-not( Test-Path -Path "$($CT_DEST)\logs\$($Script_Name)" )) {
            try{
                mkdir "$($CT_DEST)\logs\$($Script_Name)" > $null
                #Transcript-Log "New logs folder for $($Script_Name) created at $CT_DEST."
            }catch{
                #Can't create the folder, therefore cannot continue
                $line = $_.InvocationInfo.ScriptLineNumber
                write-Error "ERROR: [ML$($line)] Cannot create logs folder for $($Script_Name) in $($CT_DEST). $($_)" -Category WriteError -CategoryReason "Cannot create logs folder for $($Script_Name) in $($CT_DEST)." -ErrorId "1004" -ErrorAction Continue
                Write-Error $_ -ErrorAction Continue
                #Stop-Transcript
                $exiterror = 1004
                exit $exiterror
                throw
            }
        }

        # Check if CT registry key exists
        $global:CT_Reg_Path = "HKLM:\Software\CT\Monitoring"
        $global:CT_Reg_Key = "$($CT_Reg_Path)\$($Script_Name)"
        if(-not( Test-Path -Path $CT_Reg_Key )) {
            try{
                $CTMonitoringReg = New-Item -Path $CT_Reg_Path -Name $Script_Name -Force
                Set-ItemProperty -Path "HKLM:\Software\CT" -Name "CustomerNo" -Value $customer
            }catch{
                #Can't create the regkey, therefore cannot continue
                $line = $_.InvocationInfo.ScriptLineNumber
                write-Error "ERROR: [ML$($line)] Cannot create registry key at $($CT_Reg_Key). $($_)" -ErrorId "1005" -Category WriteError -CategoryReason "Cannot create registry key $($CT_Reg_Path)." -ErrorAction Continue
                Write-Error "$($CTMonitoringReg)" -ErrorAction Continue
                Write-Error $_
                #Stop-Transcript
                $exiterror = 1005
                exit $exiterror
            }
        }


        # Set global CT WMI class
        $global:CT_WMI_Class = "CT_$($Script_Name)"



        #Setup TLS 1.1 and 1.2
        $Name         = 'DisabledByDefault'
        $Value        = '0'

        if(-not( Test-Path -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1" )) {
            try{
                New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols" -Name "TLS 1.1" -Force | Out-Null
                New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1" -Name "Client" -Force | Out-Null
                New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client" -Name $Name -Value $Value -PropertyType DWORD -Force | Out-Null
            }catch{
                #Can't create the regkey, therefore cannot continue
                Write-Error "Cannot set TLS 1.1." -ErrorAction Continue
                $line = $_.InvocationInfo.ScriptLineNumber
                write-Error "ERROR: [ML$($line)] Cannot set TLS 1.1. $($_)" -ErrorId "1006" -Category WriteError -CategoryReason "Cannot set TLS 1.1." -ErrorAction Stop
                Write-Error $_
            }
        } else {
            Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client" -Name $Name -Value $Value | Out-Null
        }

        if(-not( Test-Path -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2" )) {
            try{
                New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols" -Name "TLS 1.2" -Force | Out-Null
                New-Item -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2" -Name "Client" -Force | Out-Null
                New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client" -Name $Name -Value $Value -PropertyType DWORD -Force | Out-Null
            }catch{
                #Can't create the regkey, therefore cannot continue
                $line = $_.InvocationInfo.ScriptLineNumber
                write-Error "ERROR: [ML$($line)] Cannot set TLS 1.2. $($_)" -ErrorId "1007" -Category WriteError -CategoryReason "Cannot set TLS 1.2." -ErrorAction Stop
                Write-Error $_
            }
        } else {
            Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client" -Name $Name -Value $Value | Out-Null
        }



        # Create a Transcript header for the verbose log
        Write-Verbose "**********************"
        Write-Verbose "Script: $($Script_Name)."
        Write-Verbose "Start time: $($DateStamp)"
        Write-Verbose "Username: $($env:USERDOMAIN)\$($env:USERNAME)"
        Write-Verbose "Execution Policy Preference: $($env:PSExecutionPolicyPreference)"
        Write-Verbose "Machine: $($env:COMPUTERNAME) ($($env:OS))"
        Write-Verbose "Process ID: $($PID)"
        Write-Verbose "PSVersion: $($PSVersionTable.PSVersion)"
        Write-Verbose "PSEdition: $($PSVersionTable.PSEdition)"
        Write-Verbose "Operating System: $($PSVersionTable.OS)"
        Write-Verbose "WSManStackVersion: $($PSVersionTable.WSManStackVersion)"
        Write-Verbose "PSRemotingProtocolVersion: $($PSVersionTable.PSRemotingProtocolVersion)"
        Write-Verbose "SerializationVersion: $($PSVersionTable.SerializationVersion)"
        Write-Verbose "**********************"


        <#
        ---- END STANDARD SCRIPT BLOCK----
        #>

    } catch {
        $line = $_.InvocationInfo.ScriptLineNumber
        Write-Error "ERROR: [ML$($line)] $($_)" -ErrorAction Stop
        Throw
    }

    #} #End process block


# This ends the script block that run on module import
# --------------------------------------------------------------------------------------------------






# --------------------------------------------------------------------------------------------------
# Initialize-Script is the old command for when the module is imported, its kept in here for backwards compatibility but it now only calls the import-module command
function Initialize-Script {
    [CmdletBinding()]
    param()

    Process{
        if($null -eq $global:Output_log) {
            Import-Module CT-PS-Standard -Force -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
        } else {
            Write-Host "Standard Module already imported - cannot import twice! Please check script $($Global:Script_Name) for import/init conflict. Script will still continue."
        }
    }
}
# --------------------------------------------------------------------------------------------------






# --------------------------------------------------------------------------------------------------
# This is used to retrieve input from the CT staff member running the script using a visual input box
Function Get-UserInput {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True, Mandatory=$True)]
        $message,
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True, Mandatory=$True)]
        $title,
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
        $defaultvalue
    )
    Process{
        [void][Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic')
        Add-Type -AssemblyName Microsoft.VisualBasic

        return [Microsoft.VisualBasic.Interaction]::InputBox($message, $title, $defaultvalue)
    }
}
# --------------------------------------------------------------------------------------------------






# --------------------------------------------------------------------------------------------------
# This is used to write output to an output log file without writing it to the console (to keep console output clean) unless -Verbose is specified
Function Write-OutputLog {
    [CmdletBinding(SupportsShouldProcess=$true)]
    Param(
        [Parameter(Mandatory,ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
        $output,
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
        [switch]$IncludeConsole,
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
        [switch]$ForceLineNumber
    )
    Process{
        $outputstring = $output
        if ($PSCmdlet.MyInvocation.BoundParameters['Verbose'] -eq $true) {
            Write-Verbose "[L$($_.InvocationInfo.ScriptLineNumber)] (Output Log) $($output)"
        }
        if ($ForceLineNumber) {
            $outputstring = "[L$($MyInvocation.ScriptLineNumber)] $($output)"
        }
        try {
            $outputstring | Out-File -FilePath "$($global:Output_log)" -Append
            if($IncludeConsole -and ($PSCmdlet.MyInvocation.BoundParameters['Verbose'] -ne $true)) {Write-Host $outputstring}
        } catch {
            Write-Host "WARNING - Could not write to Output log file: $($outputstring)"
        }
        #Write-Host $output
    }
}
# --------------------------------------------------------------------------------------------------






# --------------------------------------------------------------------------------------------------
# Writes to the API log and optionally console if -Verbose flag is set at script level
Function Write-APILog {
    [CmdletBinding(SupportsShouldProcess=$true)]
    Param(
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
        $output,
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
        [switch]$IncludeConsole,
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
        [switch]$ForceLineNumber
    )
    Process{
        $outputstring = $output
        if ($PSCmdlet.MyInvocation.BoundParameters['Verbose'] -eq $true) {
            Write-Verbose "[L$($_.InvocationInfo.ScriptLineNumber)] (API Log) $($output)"
        }
        if ($ForceLineNumber) {
            $outputstring = "[L$($MyInvocation.ScriptLineNumber)] $($output)"
        }
        try {
            $outputstring | Out-File -FilePath "$($global:API_log)" -Append
            if($IncludeConsole -and ($PSCmdlet.MyInvocation.BoundParameters['Verbose'] -ne $true)) {Write-Host $outputstring}
        } catch {
            Write-Host "WARNING - Could not write to API log file: $($outputstring)"
        }
    }
}
# --------------------------------------------------------------------------------------------------






# --------------------------------------------------------------------------------------------------
# This is used to download files using BITS, and if BITS is not available, directly using web calls.
Function Request-Download {
    [CmdletBinding(SupportsShouldProcess=$true)]
    Param(
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True, Mandatory = $true)]
        [string[]] $FILE_URL,
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True, Mandatory = $true)]
        [string[]] $FILE_LOCAL,
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
        [switch] $NoBITS, # This is for when BITS should not be used
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
        [string[]] $BasicUsername, # This is for auth for downloading
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
        [String[]] $BasicPasswd, # This is for auth for downloading
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
        [Int32] $Retry = 2 # This is for auth for downloading

    )

    Process{
        Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Attempting to download $($FILE_URL) to $($FILE_LOCAL)."

        if($Retry -lt 1) {$Retry = 2}
        if ($BasicUsername -and $BasicPasswd) {
            $Credentials = New-Object System.Management.Automation.PSCredential ($userName, (ConvertTo-SecureString $BasicPasswd -AsPlainText -Force))
        }
        # Test for existing file and remove if it exists
        if(Test-Path -Path $FILE_LOCAL) {
            try {
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Existing file found - renaming existing file."
                $FILE_NAME = Split-Path $FILE_LOCAL -Leaf
                $FILE_EXT = ($FILE_NAME).Substring(($FILE_NAME).Length-4,($FILE_NAME).Length)   
                $FILE_NAME = ($FILE_NAME).Substring(0,($FILE_NAME).Length-4)
                $FILE_NAME = "$($FILE_NAME)_old$($global:DateStamp)$($FILE_EXT)"
                Rename-Item -Path $FILE_LOCAL -NewName $FILE_NAME -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
            } catch {
                $line = $_.InvocationInfo.ScriptLineNumber
                Write-Host "ERROR: [ML$($line)] Cannot remove $($FILE_LOCAL)."
                $PScmdlet.ThrowTerminatingError($_)
            }
        }
        

        if (!($NoBITS)) {
            try {
                if ($ComputerType -ne 1) {
                    Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Installing BranchCache."
                    Install-WindowsFeature BranchCache | Out-Null
                }
            } catch {
                $NoBITS = $true
                $line = $_.InvocationInfo.ScriptLineNumber
                Write-Host "ERROR: [ML$($line)] Cannot install BranchCache."
                Write-Host $_
            }
        }

        if (!(Get-Module -ListAvailable -Name "BitsTransfer") -and !($NoBITS)) {
            try{
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Importing BitsTransfer Module."
                Import-Module BitsTransfer -Force  -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
            } catch {
                $NoBITS = $true
                $line = $_.InvocationInfo.ScriptLineNumber
                Write-Host "ERROR: [ML$($line)] Cannot install BitsTranfer."
                Write-Host $_
            }
        }

        if (!($NoBITS)) {
            # Check if BranchCache and BITS is functional, and if not, switch to NoBITS mode
            try{
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Getting BranchCache status."
                $BCStatus = Get-BCStatus -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) | Out-Null
            } catch {
                $NoBITS = $true
                $line = $_.InvocationInfo.ScriptLineNumber
                Write-Host "WARNING: [ML$($line)] BranchCache and/or BITS is corrupt and cannot be used. Regular web transfer will be attempted instead."
                Write-Host $_
    
            }
        }



        if (!($NoBITS)) {
            # Check if BranchCache Distributed Mode is enabled, and if not, enable it so BITS uses computers on the subnet to download where available
            $BitsRetry = $Retry
            if ($BCStatus.ClientConfiguration.CurrentClientMode -ne "DistributedCache") {
                try {
                    Enable-BCDistributed -Force  -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                    Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] BranchCache Distributed Mode is now enabled"
                } catch {
                    #BranchCache cannot be enabled to work with BITS. BITS will download over the internet connection instead of cached copies on the local subnet
                    #Write-OutputLog "Cannot enable BranchCache Distributed Mode. $($_). The installation files will download over the internet connection instead of cached copies on the local subnet" -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                    $line = $_.InvocationInfo.ScriptLineNumber
                    Write-Host "ERROR: [ML$($line)] Cannot enable BranchCache Distributed Mode."
                    Write-Host $_
                }
            } else {
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] BranchCache Distributed Mode is already enabled in distributed mode on this computer"
            }
            while ($BitsRetry -gt 0) {
                
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Downloading $($FILE_URL) using BITS to $($FILE_LOCAL) - Attempt $(($Retry - $BitsRetry)+1) of $($Retry)" 
                try {
                    if ($Credentials) {
                        $DownloadJob = Start-BitsTransfer -Priority Normal -DisplayName "$($DateStamp) $($FILE_LOCAL)" -Source "$($FILE_URL)" -Destination "$($FILE_LOCAL)" -Credential $Credentials -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                    } else {
                        $DownloadJob = Start-BitsTransfer -Priority Normal -DisplayName "$($DateStamp) $($FILE_LOCAL)" -Source "$($FILE_URL)" -Destination "$($FILE_LOCAL)" -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                    }
                    
                    #Complete-BitsTransfer -BitsJob $DownloadJob
                    Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Downloaded $($FILE_URL) using BITS to $($FILE_LOCAL)" # -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                    $BitsRetry = 0
                } catch {
                    $line = $_.InvocationInfo.ScriptLineNumber
                    #Write-OutputLog "Cannot download $($FILE_URL) using BITS. Now trying through standard HTTP request." -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                    #Write-OutputLog "$($_ | Out-String)" -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                    if ($BitsRetry -eq 0) {
                        Write-Host "ERROR: [ML$($line)] Failed to download $($FILE_URL) using BITS. Switching to standard web request mode."
                        $NoBITS = $true
                    } else {
                        Write-Host "WARNING: [ML$($line)] Unable to download $($FILE_URL) using BITS. Retrying."
                        $BitsRetry = $BitsRetry -1
                    }
                }
            }
        } 
        if ($NoBITS) {
            $WebRetry = $Retry
            while ($WebRetry -gt 0) {
                
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Downloading $($FILE_URL) using standard web request to $($FILE_LOCAL) - Attempt $(($Retry - $WebRetry)+1) of $($Retry)" 
                try {
                    if ($Credentials) {
                        $DownloadJob = Invoke-WebRequest -Uri "$($FILE_URL)" -OutFile "$($FILE_LOCAL)" -PassThru -Credential $Credentials -UseBasicParsing -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                    } else {
                        $DownloadJob = Invoke-WebRequest -Uri "$($FILE_URL)" -OutFile "$($FILE_LOCAL)" -PassThru -UseBasicParsing -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                    }
                    Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Downloaded $($FILE_URL) using standard web request to $($FILE_LOCAL)" 
                    $WebRetry = 0
                } catch {
                    $line = $_.InvocationInfo.ScriptLineNumber
                    if ($WebRetry -eq 0) {
                        Write-Host "ERROR: [ML$($line)] Failed to download $($FILE_URL) using standard web request."
                        Write-Host $_
                        $PScmdlet.ThrowTerminatingError($_)
                    } else {
                        Write-Host "WARNING: [ML$($line)] Unable to download $($FILE_URL) using standard web request. Retrying."
                        Write-Host $_
                        $WebRetry = $WebRetry -1
                    }
                }
            }
        }
        return $DownloadJob

    }
}
# --------------------------------------------------------------------------------------------------






# --------------------------------------------------------------------------------------------------
# Returns current script line number
function Get-CurrentLineNumber {
    [CmdletBinding()]
    param()
    return $PSCmdlet.MyInvocation.ScriptLineNumber
}
# --------------------------------------------------------------------------------------------------






# --------------------------------------------------------------------------------------------------
# Updates WMF on the machine to minimum 5.1
function Update-WMF {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter()] [switch] $ForceReboot # Forces a reboot of the machine after update has completed
    )


    $OSInfo = (Get-WMIObject win32_operatingsystem)
    $OSBuild = $OSInfo.buildnumber
    $OSArch = $OSInfo.OSArchitecture
    $PowerShellVersion = $PSVersionTable.PSVersion.Major + ($PSVersionTable.PSVersion.Minor/10)
    $dotnetversion = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" -Name Release).Release
    Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] DotNet Framework version $($dotnetversion) found."
    Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Powershell version $($PSVersionTable.PSVersion.ToString()) found."
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    
    if ($dotnetversion -lt 379893) {
        Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Updating DotNet Framework to 4.5.2"
        $dotnet_URL = "https://download.microsoft.com/download/E/2/1/E21644B5-2DF2-47C2-91BD-63C560427900/NDP452-KB2901907-x86-x64-AllOS-ENU.exe"
        $dotnet_File = "C:\CT\NDP452-KB2901907-x86-x64-AllOS-ENU.exe"
        try {
            Invoke-WebRequest -Uri $dotnet_URL -OutFile $dotnet_File -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) -UseBasicParsing
        } catch {
            $line = $_.InvocationInfo.ScriptLineNumber
            Write-Host "ERROR: [ML$($line)] Cannot download DotNet Framework 4.5.2. $($_)" # -ErrorId $_.Exception.HResult -Category ConnectionError -ErrorAction Continue
            #write-host "Cannot download DotNet Framework 4.5.2. $_"
            $PScmdlet.ThrowTerminatingError($_)
        }

        Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Installing DotNet Framework 4.5.2"
        try {
            $DotNetInstall = Start-Process -FilePath $dotnet_File -ArgumentList "/q /norestart" -Wait -NoNewWindow -PassThru -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
        } catch {
            $line = $_.InvocationInfo.ScriptLineNumber
        }
        if (@(0,3010) -contains $DotNetInstall.ExitCode) {
            Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] DotNet Framework 4.5.2 installed successfully. A reboot of this computer is required to complete the installation."
        } else {
            #write-host "Unable to install DotNet Framework 4.5.2. Error code $($DotNetInstall.ExitCode) - $_"
            Write-Host "ERROR: [ML$($line)] Unable to install DotNet Framework 4.5.2. $($_)" # -ErrorId $DotNetInstall.ExitCode -ErrorAction Continue
            #Stop-Transcript
            $PScmdlet.ThrowTerminatingError($_)
        }
    }



    if($OSBuild -eq "9600" -and $PowerShellVersion -lt 5.1) {
        # Windows 8.1 and Windows Server 2012r2
        $WMF_URL = "https://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win8.1AndW2K12R2-KB3191564-x64.msu"
    } elseif($OSBuild -eq "9200" -and $PowerShellVersion -lt 5.1) {
        # Windows 8.1 and Windows Server 2012r2
        if($OSArch -eq "64-bit"){
            $WMF_URL = "https://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/W2K12-KB3191565-x64.msu"
        } else {
            $WMF_URL = "https://download.microsoft.com/download/6/F/5/6F5FF66C-6775-42B0-86C4-47D41F2DA187/Win8.1-KB3191564-x86.msu"
        }    
    }

    if ($WMF_URL) {
        
        $WMF_File = "C:\CT\WMF51.msu"

        # Test for existing WMF file and remove if it exists
        if(Test-Path -Path $WMF_File -PathType Leaf ) {
            try {
                Remove-Item $WMF_File -Force -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                #write-host "Found old WMF update and removed."
            } catch {
                #Can't remove the WMF, therefore cannot continue
                $line = $_.InvocationInfo.ScriptLineNumber
                Write-Host "ERROR: [ML$($line)] Cannot remove $($WMF_File). $($_)" -ErrorId $_.Exception.HResult -ErrorAction Continue
                #write-host "Cannot remove $WMF_File. Unable to continue. $($Error[0].Exception.Message)"
                #Stop-Transcript
                $PScmdlet.ThrowTerminatingError($_)
            }
        }
        
        # Download WMF
        try {
            Invoke-WebRequest -Uri $WMF_URL -OutFile $WMF_File -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) -UseBasicParsing
        } catch {
            #Write-Verbose "Cannot download WMF. $($WMFjob.ErrorContextDescription)"
            $line = $_.InvocationInfo.ScriptLineNumber
            Write-Host "ERROR: [ML$($line)] Cannot download WMF. $($_)" # -ErrorId $_.Exception.HResult -ErrorAction Continue
            #Stop-Transcript
            $PScmdlet.ThrowTerminatingError($_)
        }
            

        write-host "Installing WMF"
        $WMFUpgrade = Start-Process -FilePath "C:\Windows\System32\wusa.exe" -ArgumentList "$($WMF_File) /quiet /norestart" -Wait -NoNewWindow -PassThru -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
        if (@(0,3010) -contains $WMFUpgrade.ExitCode) {
            Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] WMF installed successfully. A reboot of this computer is required to complete the installation."
        } else {
            $line = $_.InvocationInfo.ScriptLineNumber
            Write-Host "ERROR: [ML$($line)] Cannot download WMF. $($_)" # -ErrorId $WMFUpgrade.ExitCode -ErrorAction Continue
            #write-host "Unable to install WMF. Error code $($WMFUpgrade.ExitCode)"
            #Stop-Transcript
            $PScmdlet.ThrowTerminatingError($_)
        }
    }

    if ($ForceReboot) {
        Start-Sleep -Seconds 60
        Restart-Computer -Force -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
    }
    return $true

}
# --------------------------------------------------------------------------------------------------






# --------------------------------------------------------------------------------------------------
# Updates PowerShell on the machine to minimum 5.1 and then downloads newer version if available
function Update-PowerShell {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter()] [switch] $ForceReboot # Forces a reboot of the machine after update has completed
    )

    $WMFupgrade = $false
    $OSInfo = (Get-WMIObject win32_operatingsystem)
    $OSBuild = $OSInfo.buildnumber
    $PowerShellVersion = $PSVersionTable.PSVersion.Major + ($PSVersionTable.PSVersion.Minor/10)
    $dotnetversion = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" -Name Release).Release
    Write-Host "DotNet Framework version $($dotnetversion) found."
    Write-Host "Powershell version $($PSVersionTable.PSVersion.ToString()) found."
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    
    if ($dotnetversion -lt 379893) {
        $WMFupgrade = $true
    }



    if($OSBuild -eq "9600" -and $PowerShellVersion -lt 5.1) {
        # Windows 8.1 and Windows Server 2012r2
        $WMFupgrade = $true
    } elseif($OSBuild -eq "9200" -and $PowerShellVersion -lt 5.1) {
        # Windows 8.1 and Windows Server 2012r2
        $WMFupgrade = $true
    }

    if($WMFupgrade -eq $true) {
        try {
            Update-WMF 
        }
        catch {
            $line = $_.InvocationInfo.ScriptLineNumber
            Write-Host "ERROR: [ML$($line)] Failed to upgrade WMF to 5.1. Please install DotNet Framework 4.5.2 and WMF 5.1 before upgrading PowerShell. $($_)" # -ErrorId $_.Exception.HResult -Category $_.CategoryInfo.Category -CategoryReason $_.CategoryInfo.Reason -ErrorAction Continue
            write-host "Failed to upgrade WMF to 5.1. Please install DotNet Framework 4.5.2 and WMF 5.1 before upgrading PowerShell"
            $PScmdlet.ThrowTerminatingError($_)
        }
    }

    Write-Host "Now will attempt to install latest PowerShell version alongside Windows PowerShell 5.1."
    try {
        Invoke-Expression -Command "& { $(Invoke-RestMethod -Uri 'https://aka.ms/install-powershell.ps1') } -UseMSI -Quiet" -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
        #iex "& { $(irm https://aka.ms/install-powershell.ps1) } -UseMSI -Quiet"
    } catch {
        $line = $_.InvocationInfo.ScriptLineNumber
        Write-Host "ERROR: [ML$($line)] Unable to install latest PowerShell. $($_)" # -ErrorId $_.Exception.HResult -Category $_.CategoryInfo.Category -CategoryReason $_.CategoryInfo.Reason -ErrorAction Continue
        Write-Host "Unable to install latest PowerShell"
        Write-Host $_
        $PScmdlet.ThrowTerminatingError($_)
    }
    return $true

}
# --------------------------------------------------------------------------------------------------






# --------------------------------------------------------------------------------------------------
# Posts to an API and logs the result
Function New-APIPost {
    [CmdletBinding(SupportsShouldProcess=$true)]
    Param(
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True, Mandatory = $true)]
        [string[]] $BASE_URL,
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True, Mandatory = $true)]
        [string[]] $EndPoint_URL,
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True, Mandatory = $true)]
        [Parameter()] $headers,
        [Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True, Mandatory = $true)]
        [Parameter()] [hashtable] $PostData, # The hashtable that needs to be posted to the API
        [Parameter()] [int] $Retry = 2 # Attempts a retry of the post if it fails
    )

    # Post to API
    Write-OutputLog "Posting $($PostData.Count) items to API" -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
    Write-APILog "$($PostData | Out-String)" -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
    $nullfound = $false
    foreach ($APIData in $PostData) {
        #Write-OutputLog $member.name
        foreach ($APIentry in $APIData.GetEnumerator()) {
            Write-Host "$($APIentry.Name) : $($APIentry.Value)"
            if ($null -eq $APIentry.Value) {$nullfound = $true}
        }
        if($nullfound -ne $true) {
            $body = $APIData | ConvertTo-Json
        } else {
            $body = $null
        }
        Write-APILog $body -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
        if ($null -ne $body) {
            $retryCount = $Retry # performs a retry after 60 seconds if it fails
            do{
                try {
                    $SendToAPI = Invoke-WebRequest -URI "$($BASE_URL)$($EndPoint_URL)" -Method 'POST' -Headers $headers -Body $body -PassThru -Debug:$DebugPreference
                    $retryCount = 0
                    #$ReturnValue = $SendToAPI
                } catch {
                    $line = $_.InvocationInfo.ScriptLineNumber
                    Write-Host "ERROR: [ML$($line)] $($_.Exception.Message). $($_)" # -ErrorId $_.Exception.HResult -Category $_.CategoryInfo.Category -CategoryReason $_.CategoryInfo.Reason -ErrorAction Continue
                    Write-OutputLog $_.Exception.Message -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                    Write-OutputLog $_.Exception -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                    Write-OutputLog $SendToAPI -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                    Write-OutputLog $body -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                    
                    # Dig into the exception to get the Response details.
                    $line = $_.InvocationInfo.ScriptLineNumber
                    Write-Error "($($line)) API Error $($SendToAPI.StatusCode): [$($_.Exception.Response.StatusCode.value__)] - $($_.Exception.Response.StatusDescription). $($_)" -ErrorId $_.Exception.HResult -Category $_.CategoryInfo.Category -CategoryReason $_.CategoryInfo.Reason -ErrorAction Continue
                    Write-OutputLog "API Error: $($SendToAPI.StatusCode) [$($_.Exception.Response.StatusCode.value__)] - $($_.Exception.Response.StatusDescription)" -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                    $line = $_.InvocationInfo.ScriptLineNumber
                    #if ($DebugPreference -eq "Continue") { write-error "API error: $($_.Exception.Response.StatusCode.value__) - $($_.Exception.Response.StatusDescription)" -Category InvalidData -ErrorAction Continue }
                    #$exiterror = $_.Exception.Response.StatusCode.value__
                    Set-ItemProperty -Path "$($CT_Reg_Key)" -Name "API-Post-$($BASE_URL)$($EndPoint_URL)" -Value "$($_.Exception.Response.StatusCode.value__)"
                    start-sleep -Seconds 60
                    $retryCount = $retryCount - 1
                    if($retryCount -lt 1) {$PScmdlet.ThrowTerminatingError($_)}
                }
            } while ($retryCount -gt 0)
        }
    }
    
}
# --------------------------------------------------------------------------------------------------






# --------------------------------------------------------------------------------------------------
# Gets the current installed AV and its status
Function Get-AVStatus {
    
    [cmdletbinding(SupportsShouldProcess=$true,DefaultParameterSetName = "computer")]

    Param(
        #The name of a computer to query.
        [Parameter(
            Position = 0,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName,
            ParameterSetName = "computer"
            )]
        [ValidateNotNullorEmpty()]
        [string[]]$Computername = $env:COMPUTERNAME,

        #An existing CIMsession.
        [Parameter(ValueFromPipeline, ParameterSetName = "session")]
        [Microsoft.Management.Infrastructure.CimSession[]]$CimSession,

        #The default is enabled products only.
        [switch]$All
    )

    Begin {
        Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] BEGIN"

        #$CTPSModules = (Get-Module CT-PS-Standard -ListAvailable)

        #$CTPSModPath = $CTPSModules[0].ModuleBase

        #$AVSearchList = import-csv -Path "$($CTPSModPath)\antiviruslist.csv"
        Function ConvertTo-Hex {
            Param([int]$Number)
            '0x{0:x}' -f $Number
        }
        [system.Version]$OSVersion = (Get-WmiObject win32_operatingsystem -computername $Computername).version

        #initialize an hashtable of paramters to splat to Get-CimInstance
        IF ($OSVersion -ge [system.version]'6.0.0.0') 
        {
            Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] OS Windows Vista/Server 2008 or newer detected"
            $cimParams = @{
                Namespace   = "root/SecurityCenter2"
                ClassName   = "AntiVirusProduct"
# ErrorAction = "Stop"
            }
        } 
        Else 
        {
            Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Windows 2000, 2003, XP detected"
            $cimParams = @{
                Namespace   = "root/SecurityCenter"
                ClassName   = "AntiVirusProduct"
# ErrorAction = "Stop"
            }
        } # end IF ($OSVersion -ge 6.0)

        #Test for SecurityCenter(2) existance and if not, run as server
        try {
            $CIMTest = Get-CimInstance @CimParams -ErrorAction SilentlyContinue -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
            if ($CIMTest) {
                $runAsServer = $False
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] $($cimParams.Namespace) found in WMI"
            } else {
                $runAsServer = $True
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] $($cimParams.Namespace) not found in WMI"
            }
        } catch {
            $runAsServer = $True
            Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] $($cimParams.Namespace) not found in WMI"
        }

        If ($All) {
            Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Getting all AV products"
        }


        $results = @()
    } #begin

    Process {

        try {

            #Check against WMI if workstation
            if($ComputerType -eq 1 -and $runAsServer -eq $False) {

                #initialize an empty array to hold results
                $AV = @()

                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] PROCESS"
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Using parameter set: $($pscmdlet.ParameterSetName)"
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] PSBoundparameters: "
                Write-Verbose ($PSBoundParameters | Out-String)

                if ($pscmdlet.ParameterSetName -eq 'computer') {
                    foreach ($computer in $Computername) {

                        Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Querying $($computer.ToUpper())" 
                        #$cimParams.ComputerName = $computer
                        Try {
                            $AV += Get-CimInstance @CimParams -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) | Where-Object {$null -ne $_.displayName}
                        }
                        Catch {
                            Write-Warning $_
                            $cimParams.ComputerName = $null
                        }

                    } #foreach computer
                }
                else {
                    foreach ($session in $CimSession) {

                        Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Using session $($session.computername.toUpper())"
                        $cimParams.CimSession = $session
                        Try {
                            $AV += Get-CimInstance @CimParams -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) | Where-Object {$null -ne $_.displayName}
                        }
                        Catch {
                            Write-Warning $_
                            $cimParams.cimsession = $null
                        }

                    } #foreach computer
                }

                foreach ($item in $AV) {
                    Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Found $($item.Displayname)"
                    $hx = ConvertTo-Hex $item.ProductState
                    $mid = $hx.Substring(3, 2)
                    if ($mid -match "00|01") {
                        $Enabled = $False
                    }
                    else {
                        $Enabled = $True
                    }
                    $end = $hx.Substring(5)
                    if ($end -eq "00") {
                        $UpToDate = $True
                    }
                    else {
                        $UpToDate = $False
                    }

                    if(!($item.pathToSignedProductExe)) {
                        $results += $item | Select-Object @{Name = "DisplayName"; Expression = { ($_.Displayname).trim() } }, ProductState,
                        @{Name = "Enabled"; Expression = { $Enabled } },
                        @{Name = "UpToDate"; Expression = { $UptoDate } },
                        @{Name = "Path"; Expression = { $_.pathToSignedProductExe } },
                        @{Name = "Version"; Expression = { $_.VersionNumber } },
                        Timestamp,
                        @{Name = "Computername"; Expression = { $_.PSComputername.toUpper() } }
                    } else {
                        if($AVproduct.displayName -match "Defender"){
                            $DefenderInfo = Get-MpComputerStatus -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                            $AVversion = $DefenderInfo.AMProductVersion
                        } else {
                            if(Test-Path -Path $item.pathToSignedProductExe) {
                                $AVversion = (Get-Item $item.pathToSignedProductExe -ErrorAction Stop).VersionInfo.fileversion
                                $results += $item | Select-Object @{Name = "DisplayName"; Expression = { ($_.Displayname).trim() } }, ProductState,
                                @{Name = "Enabled"; Expression = { $Enabled } },
                                @{Name = "UpToDate"; Expression = { $UptoDate } },
                                @{Name = "Path"; Expression = { $_.pathToSignedProductExe } },
                                @{Name = "Version"; Expression = { $AVversion } },
                                Timestamp,
                                @{Name = "Computername"; Expression = { $_.PSComputername.toUpper() } }
                            }
                        }
                    }

                } #foreach
            
            } else {

                
                $ModulePath = (Get-Module -ListAvailable -Name CT-PS-Standard).ModuleBase
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] ModulePath: $($ModulePath)"
                $vbsexe = Invoke-Expression -Command "CMD.exe /c CSCRIPT '$($ModulePath)\avstatus.vbs' WRITE" -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) -ErrorAction Stop
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] $($vbsexe)"

                $AV = @()
                $cimParams = @{
                    Namespace   = "root/SecurityCenter"
                    ClassName   = "AntiVirusProduct"
    # ErrorAction = "Stop"
                }

                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Using parameter set: $($pscmdlet.ParameterSetName)"
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] PSBoundparameters: "
                Write-Verbose ($PSBoundParameters | Out-String)

                if ($pscmdlet.ParameterSetName -eq 'computer') {
                    foreach ($computer in $Computername) {

                        Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Querying $($computer.ToUpper())"
                        #$cimParams.ComputerName = $computer
                        Try {
                            $AV += Get-CimInstance @CimParams -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) | Where-Object {$null -ne $_.displayName}
                        }
                        Catch {
                            Write-Warning $_
                            $cimParams.ComputerName = $null
                        }

                    } #foreach computer
                }
                else {
                    foreach ($session in $CimSession) {

                        Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Using session $($session.computername.toUpper())"
                        $cimParams.CimSession = $session
                        Try {
                            $AV += Get-CimInstance @CimParams -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
                        }
                        Catch {
                            Write-Warning $_
                            $cimParams.cimsession = $null
                        }

                    } #foreach computer
                }

                foreach ($item in $AV) {
                    Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Found $($item.Displayname)"
                    $hx = ConvertTo-Hex $item.ProductState
                    $mid = $hx.Substring(3, 2)
                    if ($mid -match "00|01") {
                        $Enabled = $False
                    }
                    else {
                        $Enabled = $True
                    }
                    $end = $hx.Substring(5)
                    if ($end -eq "00") {
                        $UpToDate = $True
                    }
                    else {
                        $UpToDate = $False
                    }

                    if(!($item.pathToSignedProductExe)) {
                        $results += $item | Select-Object @{Name = "DisplayName"; Expression = { ($_.Displayname).trim() } }, ProductState,
                        @{Name = "Enabled"; Expression = { $Enabled } },
                        @{Name = "UpToDate"; Expression = { $UptoDate } },
                        @{Name = "Path"; Expression = { $_.pathToSignedProductExe } },
                        @{Name = "Version"; Expression = { $_.VersionNumber } },
                        Timestamp,
                        @{Name = "Computername"; Expression = { $_.PSComputername.toUpper() } }
                    } else {
                        if($AVproduct.displayName -match "Defender"){
                            $DefenderInfo = Get-MpComputerStatus
                            $AVversion = $DefenderInfo.AMProductVersion
                        } else {
                            if(Test-Path -Path $item.pathToSignedProductExe) {
                                $AVversion = (Get-Item $item.pathToSignedProductExe -ErrorAction Stop).VersionInfo.fileversion
                                $results += $item | Select-Object @{Name = "DisplayName"; Expression = { ($_.Displayname).trim() } }, ProductState,
                                @{Name = "Enabled"; Expression = { $Enabled } },
                                @{Name = "UpToDate"; Expression = { $UptoDate } },
                                @{Name = "Path"; Expression = { $_.pathToSignedProductExe } },
                                @{Name = "Version"; Expression = { $AVversion } },
                                Timestamp,
                                @{Name = "Computername"; Expression = { $_.PSComputername.toUpper() } }
                            }
                        }
                    }

                } #foreach

            } #if/else
        } catch {
            $line = $_.InvocationInfo.ScriptLineNumber
            Write-Host "ERROR: [ML$($line)] Unable to retrieve AV information"
            write-host $_
            $PScmdlet.ThrowTerminatingError($_)
        }

    } #process

    End {
        If ($All) {
            Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Returning:"
            Write-Verbose "$($results | Out-String)"
            return $results
        } else {
            #filter for enabled only
            Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Returning:"
            Write-Verbose "$(($results).Where( { $_.enabled }) | Out-String)"
            return ($results).Where( { $_.enabled })
        }

        Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] END"
    } #end

} #end function
# --------------------------------------------------------------------------------------------------







# --------------------------------------------------------------------------------------------------
# This will interactively prompt to select one or more items from a list

function Select-FromChoices {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory,ValueFromPipelineByPropertyName,HelpMessage="Array of options")]
        [array] $CIMInput, # Array of options to present
        [Parameter(Mandatory,ValueFromPipelineByPropertyName,HelpMessage="Title text")]
        [string] $TitleText, # Array of options to present
        [Parameter(ValueFromPipelineByPropertyName,HelpMessage="Should the box be multi select or not?")]
        [bool] $Multiselect = $true # Multiselect?
    )

    Process{
        try{
            Add-Type -AssemblyName System.Windows.Forms
            Add-Type -AssemblyName System.Drawing
            
            $Screens = [System.Windows.Forms.Screen]::AllScreens
            $ScreenSize = $Screens[0].Bounds.Size
            $ScreenWidth = $ScreenSize.Width
            $ScreenHeight = $ScreenSize.Height
            $FormWidth = $ScreenWidth / 3
            if($FormWidth -lt 400) {$FormWidth = 400}
            $FormHeight = $ScreenHeight / 3
            if($FormHeight -lt 400) {$FormHeight = 400}
            $form = New-Object System.Windows.Forms.Form
            $form.Text = $TitleText
            $form.Size = New-Object System.Drawing.Size($FormWidth,$FormHeight)
            $form.StartPosition = 'CenterScreen'
            $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
            $form.MaximizeBox = $false
            $form.MinimizeBox = $false
            
            $OKButton = New-Object System.Windows.Forms.Button
            $OKVPos = $FormHeight-70
            $OKHPos = 20
            $OKButton.Location = New-Object System.Drawing.Point($OKHPos,$OKVPos)
            $OKButton.Size = New-Object System.Drawing.Size(75,23)
            $OKButton.Text = 'OK'
            $OKButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
            $form.AcceptButton = $OKButton
            $form.Controls.Add($OKButton)
            
            $CancelButton = New-Object System.Windows.Forms.Button
            $CancelVPos = $FormHeight-70
            $CancelHPos = $FormWidth-115
            $CancelButton.Location = New-Object System.Drawing.Point($CancelHPos,$CancelVPos)
            $CancelButton.Size = New-Object System.Drawing.Size(75,23)
            $CancelButton.Text = 'Cancel'
            $CancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
            $form.CancelButton = $CancelButton
            $form.Controls.Add($CancelButton)
            
            $label = New-Object System.Windows.Forms.Label
            $label.Location = New-Object System.Drawing.Point(20,10)
            $label.Size = New-Object System.Drawing.Size(370,20)
            if($Multiselect -eq $true) {
                $label.Text = 'Select from the list below: (hold control to select multiple options)'
            } else {
                $label.Text = 'Select from the list below:'
            }
            $form.Controls.Add($label)
            
            $listBox = New-Object System.Windows.Forms.Listbox
            $ListHeight = $FormHeight-120
            $ListWidth = $FormWidth-56
            $listBox.Location = New-Object System.Drawing.Point(20,40)
            $listBox.Size = New-Object System.Drawing.Size($ListHeight,$ListWidth)
            
            if($Multiselect -eq $true) {
                $listBox.SelectionMode = 'MultiExtended'
            }
            
            foreach($tempSKU in $CIMInput)
            {
                [void] $listBox.Items.Add($tempSKU)
            }
            
            $listBox.Height = $ListHeight #70
            $listBox.Width = $ListWidth #70
            $form.Controls.Add($listBox)
            $form.Topmost = $true
            
            $result = $form.ShowDialog()
            
            if ($result -eq [System.Windows.Forms.DialogResult]::OK)
            {
                $x = $listBox.SelectedItems
            }
            else{
                $x = $null
            }

            return $x
        } catch {
            $line = $_.InvocationInfo.ScriptLineNumber
            Write-Host "ERROR: [ML$($line)] $($_.Exception.Message). $($_)"
            $PScmdlet.ThrowTerminatingError($_)
        }
    }
}

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







# --------------------------------------------------------------------------------------------------
Function Set-WMIValue {
<#
This function is used to set a WMI property using CIM. If the namespace or class do not exist, it will be created.
 
All parameters can be pipelined by their respective propertyname
 
PARAMETERS:
-Namespace: Specifies the namespace where to search for the WMI class. Default is: 'ROOT\cimv2'.
-ClassName: Specifies the class where to create the new WMI instance.
-KeyID: (Optional) Specifies the property within the $Property hashtable that is used as the key. If an instance with the key exists, it will be updated. If it does not, it will be created as a new instance.
                If omitted, the current datetime and calling script will be used as the key value for creating a new instance using the key property "CT_Key"
-Property: Specifies the class instance Properties or Values.
                Example of hashtable:
                [hashtable]$Property = @{
                    'ServerPort' = '89'
                    'ServerIP' = '11.11.11.11'
                    'Source' = 'File1'
                    'Date' = $(Get-Date)
                }
 
 
EXAMPLES:
[hashtable]$Property = @{
        'ServerPort' = '89'
        'ServerIP' = '11.11.11.11'
        'ServerName' = 'File1'
        'Date' = $(Get-Date)
    }
Set-WMIValue -Namespace "ROOT\cimv2" -ClassName "CORP_Servers" -KeyID "ServerName" -Property $Property
 
#>

[CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(ValueFromPipelineByPropertyName,HelpMessage="WMI Namespace (root\CIMV2 if not specified)")]
        [ValidateNotNullorEmpty()]
        [string]$Namespace = 'ROOT\cimv2',
        [Parameter(ValueFromPipelineByPropertyName,HelpMessage="Class Name")]
        [ValidateNotNullorEmpty()]
        [string]$ClassName,
        [Parameter(ValueFromPipelineByPropertyName,HelpMessage="Key Property Name")]
        [ValidateNotNullorEmpty()]
        [string]$KeyID,
        [Parameter(ValueFromPipelineByPropertyName,HelpMessage="CIM property hashtable")]
        [ValidateNotNullorEmpty()]
        [hashtable] $Property
    )

    Process{

        try{

            $line = $_.InvocationInfo.ScriptLineNumber
            Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Set-WMIValue function triggered with the following parameters:
            Namespace: $($Namespace)
            ClassName: $($ClassName)
            KeyID: $($KeyID)
            KeyValue: $($KeyValue)
            Properties:
            $($Property | Out-String)
            "


            ### Check if the namespace exists and create if not
            $line = $_.InvocationInfo.ScriptLineNumber
            Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Checking to see if Namespace '$($Namespace)' exists"

            $SplitNamespace = $Namespace.Split("\")
            if ($SplitNamespace[0] -notlike "root") {
                $line = $_.InvocationInfo.ScriptLineNumber
                Write-Host "ERROR: [ML$($line)] Invalid namespace path '$($SplitNamespace[0])'. Namespace should always start with 'ROOT'."
                $PScmdlet.ThrowTerminatingError("ERROR: [ML$($line)] Invalid namespace path '$($SplitNamespace[0])'. Namespace should always start with 'ROOT'.")
            }
            $NamespaceTest = get-wmiobject -namespace $SplitNamespace[0] -Class __NAMESPACE -Filter "name='$($SplitNamespace[1])'" -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) -ErrorAction 'SilentlyContinue'
# $NamespaceTest = Get-WmiNamespace -Namespace $Namespace -ErrorAction 'SilentlyContinue'

            If (!$NamespaceTest) {
                $line = $_.InvocationInfo.ScriptLineNumber
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] '$($Namespace)' does not exist - creating new Namespace"
                $NameSpaceObject = (New-Object -TypeName 'System.Management.ManagementClass' -ArgumentList "\\.\$($SplitNamespace[0])`:__NAMESPACE").CreateInstance()
                $NameSpaceObject.Name = $SplitNamespace[1]

                # Write the namespace object
                $NewNamespace = $NameSpaceObject.Put()
                $NameSpaceObject.Dispose()
            }



            ### Check if the class exists and create if not
            $line = $_.InvocationInfo.ScriptLineNumber
            Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Checking to see if Class '$($ClassName)' exists"
            $ClassTest = Get-CimClass -Namespace $Namespace -ClassName $ClassName  -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true) -ErrorAction 'SilentlyContinue'

            If (!$ClassTest) {

                try{
                    $line = $_.InvocationInfo.ScriptLineNumber
                    Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] '$($ClassName)' does not exist - creating new Class"
                    # Create class object
                    [wmiclass]$ClassObject = New-Object -TypeName 'System.Management.ManagementClass' -ArgumentList @("\\.\$Namespace`:__CLASS", [String]::Empty, $null)
                    $ClassObject.Name = $ClassName

                    # Write the class and dispose of the class object
                    $NewClass = $ClassObject.Put()
                    $ClassObject.Dispose()

                } catch {
                    # On class creation failure, write debug message and optionally throw error if -ErrorAction 'Stop' is specified

                    # Error handling and logging
                    $line = $_.InvocationInfo.ScriptLineNumber
                    Write-Host "ERROR: [ML$($line)] Failed to create class [$ClassName] in namespace [$Namespace]."
                    Write-Host $_
                    $PScmdlet.ThrowTerminatingError($_)
                }

            }
            ### End class check and creation


            ### Check if key exists and if not, set

            # First check for or set the Key and its respective value
            if($KeyID) {
                $line = $_.InvocationInfo.ScriptLineNumber
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Checking to see if key '$($KeyID)' exists in provided properties"
                # Check if instance exists with the provided key
                try{
                   $KeyValue = $Property[$KeyID]
                } catch {
                    #Key doesn't exist in the hashtable, will switch to creating default one
                    $line = $_.InvocationInfo.ScriptLineNumber
                    Write-Host "WARNING: [ML$($line)] $($KeyID) doesn't exist in the Property hashtable. Will use default key of 'CT_Key' and value '$($DateStamp)$($Script_Name)'."
                }
            }
            if (-not $KeyValue) {
                $line = $_.InvocationInfo.ScriptLineNumber
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Creating key 'CT_Key' and adding to provided properties"
                $KeyID = "CT_Key"
                $KeyValue = "$($DateStamp)$($Script_Name)"
                $NewProperty = @{$KeyID = $KeyValue}
                $NewProperty += $Property
                $Property = $NewProperty
            }
            ### End key check





            ### If the Class is new, add all the properties to the Class
            If ($NewClass) {
                try{
                    $line = $_.InvocationInfo.ScriptLineNumber
                    Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Checking all provided properties exist in class '$($ClassName)' within namespace '$($Namespace)'."

                    Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Adding key property '$($KeyID)'"

                    [wmiclass]$ClassObject = New-Object -TypeName 'System.Management.ManagementClass' -ArgumentList @("\\.\$Namespace`:$ClassName")
            
                    # Add class property
                    $PropertyIsArray = $false
                    $PropertyType = switch (($Property[$KeyID].GetType()).Name) {
                        "sbyte" { "SInt8"; break }
                        "Int16" { "SInt16"; break }
                        "Int32" { "SInt32"; break }
                        "Int" { "SInt32"; break }
                        "Int64" { "SInt64"; break }
                        "Byte" { "UInt8"; break }
                        "Single" { "Real32"; break }
                        "Double" { "Real64"; break }
                        "TimeSpan" { "DateTime"; break }
                        "Char" { "Char16"; break }
                        Default {($Property[$KeyID].GetType()).Name}
                    }
                    
                    
                    $ClassObject.Properties.Add($KeyID, [System.Management.CimType]$PropertyType, $PropertyIsArray)
                    $ClassObject.Properties[$KeyID].Qualifiers.Add('read',$true)
                    $ClassObject.Properties[$KeyID].Qualifiers.Add('write',$true)
                    $ClassObject.Properties[$KeyID].Qualifiers.Add('key',$true)
    
                    # Write class object
                    try {
                        $null = $ClassObject.Put()
                        $line = $_.InvocationInfo.ScriptLineNumber
                        Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Added property '$($KeyID)' to class '$($ClassName)' within namespace '$($Namespace)'."
                    } catch {
                        $line = $_.InvocationInfo.ScriptLineNumber
                        Write-Host "ERROR: [ML$($line)] Unable to create property '$($PropekeyrtyName)' in class '$($ClassName)' within namespace '$($Namespace)'."
                        Write-Host $_
                        $PScmdlet.ThrowTerminatingError($_)
                    }
                    $ClassObject.Dispose()

                    ForEach ($PropertyName in $Property.Keys | where {$_ -ne $KeyID}) {
                        $line = $_.InvocationInfo.ScriptLineNumber

                        # Set property to array if specified
                        $PropertyType = switch (($Property[$PropertyName].GetType()).Name) {
                            "sbyte" { "SInt8"; break }
                            "Int16" { "SInt16"; break }
                            "Int32" { "SInt32"; break }
                            "Int" { "SInt32"; break }
                            "Int64" { "SInt64"; break }
                            "Byte" { "UInt8"; break }
                            "Single" { "Real32"; break }
                            "Double" { "Real64"; break }
                            "TimeSpan" { "DateTime"; break }
                            "Char" { "Char16"; break }
                            Default {($Property[$PropertyName].GetType()).Name}
                        }
                        If ($PropertyType -match 'Array') {
                            $PropertyType = $PropertyType.Replace('Array','')
                            $PropertyIsArray = $true
                        } Else {
                            $PropertyIsArray = $false
                            #$PropertyType = "String"
                        }
        
                        # Create the ManagementClass object
                        [wmiclass]$ClassObject = New-Object -TypeName 'System.Management.ManagementClass' -ArgumentList @("\\.\$Namespace`:$ClassName")
        
                        # Add class property
                        $ClassObject.Properties.Add($PropertyName, [System.Management.CimType]$PropertyType, $PropertyIsArray)
                        $ClassObject.Properties[$PropertyName].Qualifiers.Add('read',$true)
                        $ClassObject.Properties[$PropertyName].Qualifiers.Add('read',$true)
                        $ClassObject.Properties[$PropertyName].Qualifiers.Add('write',$true)
        
                        # Write class object
                        try {
                            $null = $ClassObject.Put()
                            $line = $_.InvocationInfo.ScriptLineNumber
                            Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Added property '$($PropertyName)' to class '$($ClassName)' within namespace '$($Namespace)'."
                        } catch {
                            $line = $_.InvocationInfo.ScriptLineNumber
                            Write-Host "ERROR: [ML$($line)] Unable to create property '$($PropertyName)' in class '$($ClassName)' within namespace '$($Namespace)'."
                            Write-Host $_
                            Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Removing property '$($PropertyName)' from the list of Properties to send to WMI class '$($ClassName)' within namespace '$($Namespace)'."
                            $Property.Remove($PropertyName)
                        }
                        $ClassObject.Dispose()
                    }
                } catch {
                    ## Catch any errors and log
                    $line = $_.InvocationInfo.ScriptLineNumber
                    Write-Host "ERROR: [ML$($line)] Unable to add properties to existing class '$($ClassName)' within namespace '$($Namespace)'."
                    Write-Host $_
                    $PScmdlet.ThrowTerminatingError($_)
                }
            }
            ### End property setup





            ### Check if instance exists using key/value pair and create if not
            $line = $_.InvocationInfo.ScriptLineNumber
            $CIMQuery = "Select * from $($ClassName) where $($KeyID) LIKE '$($KeyValue)'"
            Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Checking if an instance with key value '$($KeyValue)' exists in class '$($ClassName)' within namespace '$($Namespace)'."
            Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] WMI Query: $($CIMQuery)."

            $InstanceTest = Get-CimInstance -Namespace $Namespace -Query $CIMQuery -ErrorAction 'SilentlyContinue' -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
            if (!$InstanceTest) {
                #Instance doesn't exist so it needs to be created. create it with its properties
                $line = $_.InvocationInfo.ScriptLineNumber
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Instance does not exist with key value '$($KeyValue)' in class '$($ClassName)' within namespace '$($Namespace)'."
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Adding a new instance."
                $Instance = New-CimInstance -Namespace $Namespace -ClassName $ClassName -Key $KeyID -Property $Property -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
            } else {
                #Instance exists so update it with new properties
                $line = $_.InvocationInfo.ScriptLineNumber
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Instance exist with key value '$($KeyValue)' in class '$($ClassName)' within namespace '$($Namespace)'."
                Write-Verbose "[ML$($_.InvocationInfo.ScriptLineNumber)] Updating existing instance."
                $Instance = Set-CimInstance -Namespace $Namespace -Query $CIMQuery -Property $Property -PassThru -Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
            }


        } catch {
            ## Catch any errors and log
            $line = $_.InvocationInfo.ScriptLineNumber
            Write-Host "ERROR: [ML$($line)] Unable to populate WMI Values in class '$($ClassName)' within namespace '$($Namespace)'."
            Write-Host $_
            $PScmdlet.ThrowTerminatingError($_)
    
        }
    }
}