Deploying Windows 10 with System Center Configuration Manager (SCCM)

There are a number of different ways Configuration Manager can be used to Deploy Windows 10.

  • In-place Upgrade: Windows 7, 8, 8.1, or 10 to the latest version. The upgrade process retains the applications, settings, and user data on the computer.
  • Refresh an existing computer (Wipe and Transfer Settings): Wipe an existing computer and installs a new operating system on the computer. You can migrate settings and user data after the operating system is installed.
  • Bare Metal Install on a New Computer: Install Windows on a new computer.
  • Replace an existing computer and transfer settings: Install Windows on a new computer. Optionally, you can migrate settings and user data from the old computer to the new computer.

https://i0.wp.com/www.systemcenterdudes.com/wp-content/uploads/2016/02/11924-01.png?resize=704%2C652&ssl=1

In-place Upgrade

This method is used to upgrade Windows 7, 8, 8.1 to 10. You can also do build-to-build upgrades of Windows 10 such as 1607 to 1709. Additionally, starting in Configuration Manager 1802, the Windows 10 in-place upgrade task sequence supports deployment to internet-based clients managed through the cloud management gateway (CMG). This method is the most robust and has no external dependencies, such as the Windows ADK

With the in-place upgrade you:

  • CANNOT change domain membership
  • CANNOT change disk partitions
  • CANNOT change architecture (x86 to x64)

Preparing the Upgrade Package

  1. Add operating system upgrade packages to Configuration Manager (the extracted ISO path)

    Image result for Operating System upgrade packages.
  2. Distribute operating system images to a distribution point
  3. Apply software updates to an operating system upgrade package

    Image result for Operating System Upgrade Packages group, click Schedule Updates

Create an In-place Upgrade Task Sequence

  1. Create a New Task Sequence -> Upgrade an operating system from upgrade package

  2. Select he Upgrade Package from the previous step
  3. Set Product key, Include Updates and Install Applications preferences
  4. Complete the wizard
  5. Add steps to get the previous OS in an upgrade state, such as

    1. Battery checks
    2. Network/wired connection checks
    3. Remove incompatible applications
    4. Remove incompatible drivers
    5. Remove/suspend third-party antivirus.

Pre-Cache Content on Client

You can optionally pre-deploy the Upgrade Package to clients so that they do not have to download it if they click to install it in Software Center. This is called pre-cache of the content.

Download Package Content Step

You can also use the Add Download Package Content step to customize such things as detect the client architecture, hardware type for each driver package.

Deploy the Task Sequence to Computers

There are two ways in which you can deploy this task sequence to computers

  • Use Software Center to deploy over the network
  • Use USB drive to deploy without using the network
  • Cloud Management Gateway (1802 and above)

Deploy with Software Center

  1. Task Sequence -> Deploy
  2. Set Deployment Settings, Scheduling, User Experience, Distribution Points
  3. With 1802 you can now also choose to save this deployment as a template.

Deploy with USB Drive

  1. Select your task sequence and choose Create Task Sequence Media

Deploy with Cloud Management Gateway (CMG)

Starting with Configuration Manager 1802, you can use a CMG to deploy a task sequence.

  1. Ensure all of the content referenced by the in-place upgrade task sequence is distributed to a cloud distribution point.
  2. When deploying the task sequence, check the additional options:

    1. Allow task sequence to run for client on the Internet, on the User Experience tab of the deployment.
    2. Download all content locally before starting task sequence, on the Distribution Points tab of the deployment.
    3. Other options such as Download content locally when needed by the running task sequence do not work in this scenario.
    4. Pre-download content for this task sequence, on the General tab of the deployment

Refresh an Existing Computer (Wipe and Transfer Settings)

Let’s look at how we can partition and format (wipe) an existing computer and install a new operating system onto it and transfer the settings as well. We need to install a state migration point to store and restore user settings on the new operating system after it is installed. This is called a Refresh

Prepare for a Refresh Scenario

There are several infrastructure requirements that must be in place before we can deploy operating systems with Configuration Manager.

Dependencies External to Configuration Manager

  • Windows ADK for Windows 10
    • User State Migration Tool (USMT) (transfer user settings)
    • WinPE images (PXE boot media)
  • Windows Server Update Services (WSUS) (for updates during deployment)
  • Windows Deployment Services (WDS) (PXE boot environment)

    • Also DHCP enabled
  • Internet Information Services (IIS) on the site system server
  • Device drivers ready

Configuration Manger Dependences

  • OS Image
  • Driver catalog (import the device driver, enable it, and make it available on a distribution point)
  • Management point
  • PXE-enabled distribution point
  • Install a State Migration Point and configure it

Prepare WinPE Boot Image

  1. Two default images are provided by Configuration Manager in \\servername>\SMS_<sitecode>\osd\boot\<x64> or <i386>
  2. Add Boot Image (Operating Systems -> Boot Images)

  3. Distribute Boot Images to Distribution Points
  4. Boot Image Properties -> Data Source tab, select Deploy this boot image from the PXE-enabled distribution point

Prepare an Operating System Image

  1. Decide to use the default install.wim or capture a reference computer and make your own .wim file.
  2. Add the Operating System Image to Configuration Manager (Operating Systems -> Add Operating System Image)

  3. Distribute the Operating System to a Distribution Point
  4. Schedule Software Updates to the Operating System Image

    Image result for Operating System Upgrade Packages group, click Schedule Updates

Create a Task Sequence

  1. Create a New Task Sequence -> Install an existing image package

  2. Choose setting such as Image package (and image), partitions, product key
  3. Choose to Join A Domain
  4. Choose to install the Configuration Manager Client
  5. Choose Under State Migration

    1. Capture user settings: capture the user state.
    2. Capture network settings: Captures network settings
    3. Capture Microsoft Windows settings: Capture the computer name, registered user and organization name, and the time zone settings.

Deploy the Task Sequence

You have a few different options to choose when deploying the task sequence.

  • Use PXE to deploy over the network
  • Use Multicast to deploy over the network
  • Use Software Center to deploy over the network
  • Create an image for an OEM in factory or a local depot

    1. Create Task Sequence Media
  • Use USB Drive to deploy Windows over the network

Bare Metal Install on a New Computer

Let’s look at how we can partition and format (wipe) a new computer and install a new operating system onto it.

Prepare for a Bare Metal Scenario

There are several infrastructure requirements that must be in place before we can deploy operating systems with Configuration Manager.

Dependencies External to Configuration Manager

  • Windows ADK for Windows 10
    • User State Migration Tool (USMT) (transfer user settings)
    • WinPE images (PXE boot media)
  • Windows Server Update Services (WSUS) (for updates during deployment)
  • Windows Deployment Services (WDS) (PXE boot environment)

    • Also DHCP enabled
  • Internet Information Services (IIS) on the site system server
  • Device drivers ready

Configuration Manger Dependences

  • OS Image
  • Driver catalog (import the device driver, enable it, and make it available on a distribution point)
  • Management point
  • PXE-enabled distribution point

Everything else here is the same as a Refresh. Follow the directions above, skipping the sections where we would use the State Migration Point.

Replace an existing computer and transfer settings

Let’s look at how we can partition and format (wipe) a destination computer and install a new operating system onto it and transfer the settings as well from a source computer. We need to install a state migration point to store and restore user settings on the new operating system after it is installed. This is called a Refresh

Prepare for a Replace Scenario

There are several infrastructure requirements that must be in place before we can deploy operating systems with Configuration Manager.

Dependencies External to Configuration Manager

  • Windows ADK for Windows 10
    • User State Migration Tool (USMT) (transfer user settings)
    • WinPE images (PXE boot media)
  • Windows Server Update Services (WSUS) (for updates during deployment)
  • Windows Deployment Services (WDS) (PXE boot environment)

    • Also DHCP enabled
  • Internet Information Services (IIS) on the site system server
  • Device drivers ready

Configuration Manger Dependences

  • OS Image
  • Driver catalog (import the device driver, enable it, and make it available on a distribution point)
  • Management point
  • PXE-enabled distribution point
  • Install a State Migration Point and configure it

Configure State Migration Point

The scenario for a replace is similar to a restore. The exception is we need to configure the State Migration Point with settings to assure we have a spot to store our migration data that is not on the computer.

http://apprize.info/microsoft/system/system.files/image288.jpg

We must specify:

  1. The drive on the server to store the user state migration data.
  2. The maximum number of clients that can store data on the state migration point.
  3. The minimum free space for the state migration point to store user state data.
  4. The deletion policy for the role. Either specify that the user state data is deleted immediately after it is restored on a computer, or after a specific number of days after the user data is restored on a computer.
  5. Whether the state migration point responds only to requests to restore user state data. When you enable this option, you cannot use the state migration point to store user state data.

by David Maiolo 2018-03-2018

Overview Co-management for Windows 10 Devices

Starting with Configuration Manager 1710, co-management allows you to concurrently manage Windows 10 1709 by using both Configuration Manager and Intune. It’s a solution that provides a bridge from traditional to modern management and allows a phased transition between the two products.

There are two major paths you can take to co-management:

  • Configuration Manager provisioned co-management: Windows 10 devices managed by Configuration Manager and hybrid Azure AD joined get enrolled into Intune.
  • Intune provisioned devices that are enrolled in Intune: Install with the Configuration Manager client becomes a co-management state.

Prerequisites:

  • Configuration Manager version 1710 +
  • Windows 10 1709 (Fall Creators Update) devices
  • Azure AD
  • EMS or Intune license for all users
  • Azure AD automatic enrollment enabled
  • Intune subscription (MDM authority in Intune set to Intune)
  • If Configuration Manager client is installed: Hybrid Azure AD joined (joined to AD and Azure AD)
  • If Configuration Manager client is NOT installed: Cloud Management Gateway

Hybrid vs Co-Management

Although they sound similar, they are not the same thing. Co-management lets you concurrently manage devices in both Intune and Configuration Manager Console. Hybrid MDM with Configuration Manager integrates Intune’s MDM capabilities into Configuration Manager. In Hybrid, you can no longer use the Intune console.

If you have a hybrid MDM environment), you cannot enable co-management. You would need to first migrate to Intune standalone.

What Intune Can Manage with Co-Management

Once co-management is enabled, Configuration Manger still performs all of the traditional tasks that it always has. Now, Intune can also manage:

  • Compliance Policies (compliance for Conditional Access)
  • Windows Update for Business Policies
  • Resource Access Policies (policies which configure VPN, email and certificate settings)

Intune can also perform the following remote tasks on the Windows 10 devices:

  • Factory reset
  • Selective wipe
  • Delete devices
  • Restart device
  • Fresh start

How to Enable Co-Management

Co-management can be enabled for Windows 10 devices both when they are enrolled in Intune and when they are existing Configuration Manager Clients. Either result a Windows 10 device concurrently managed by Configuration Manager and Intune (as well as joined to both AD and Azure AD).

Windows 10 Devices enrolled in Intune

When devices are enrolled in Intune first, you can install the Configuration Manager client on the devices by creating a new line-of-business spp in Intune and use your ccmsetup.msi file with the following command line:


ccmsetup.msi 
CCMSETUPCMD="/mp:/ CCMHOSTNAME= 
SMSSiteCode= 
SMSMP=https:// 
AADTENANTID= 
AADTENANTNAME= 
AADCLIENTAPPID= 
AADRESOURCEURI=https://

Then, you enable co-management from the Configuration Manager console.

Brand New Windows 10 Devices

For new devices you can use Windows AutoPilot to configure the Out of Box Experience (OOBE), which includes automatic enrollment that enrolls devices in Intune.

First, create a new Windows AutoPilot Deployment Program profile in intune:

Then, find the devices you want the profile enabled for and assign the profile to those devices.

Windows 10 Configuration Manager Clients

You can enroll these devices and enable co-management from the Configuration Manager console. Configuration Manager starts automatic enrollment into Intune based on the Azure AD tenant they belong to.

Configure Configuration Manager for Co-Management

A few things are left to be done. First, we need to enable co-management in the Configuration Manager Console. Then, we need to start switching specific Configuration Manager workloads to Intune.

  1. In the Configuration Manager console, go to Administration > Overview > Cloud Services > Co-management then click  Configure co-management to open the Co-management Configuration Wizard.
  2. Sign In to your Intune tenant, and then click Next.
  3. On the Enablement page, choose either Pilot or All to enable Automatic enrollment in Intune, and then click Next.
  4. On the Workloads page, choose to switch Configuration Manager workloads to be managed by Pilot Intune or Intune, and then click Next.
  5. To enable co-management, complete the wizard.

https://i0.wp.com/jerrymeyer.nl/wp-content/uploads/2017/11/SCCM_CB_1709_Co_Management_Setup_3.jpg?resize=650%2C341&ssl=1

Check compliance for co-managed devices

Use the Software Center to detect compliance for co-managed Windows 10 devices. You can check this compliance regardless of whether conditional access is managed by Configuration Manager or Intune. You can also check compliance with the Company Portal app when conditional access is managed by Intune.

by David Maiolo 2018-03-16

Cloud-Based Management Service Overview

Internet-based client management has been available for years in Configuration Manger, however it’s generally not very easy to setup, with an estimated 10% of Microsoft’s Configuration Manager install-base having actually used it.

Starting with the Configuration Manager 1610 release, management of internet-based clients is now available through an Azure hosted service called the Cloud Management Gateway. This is done through a new role called the cloud management gateway connector point. Once the role is added in Configuration Manger, it becomes the point your internet-based clients proxy back into your On-Premises Configuration Manager services, or your Azure hosted Configuration Manger services.

The strategic goal in adding the Cloud Management Gateway to your environment is to provide an intermediary cloud solution on your roadmap to a full cloud management solution of your Windows 10 devices through Microsoft Intune. In this intermediary stage you still have access to your traditional agent based management from Configuration Manger while extending the perimeter for clients that roam on the internet. The Cloud Management Gateway is accomplished without adding additional infrastructure and without exposing any of your infrastructure to the internet.

The one drawback to this service from traditional Internet-Based client management is that it requires a Microsoft Azure monthly subscription for the cloud service.

Deployment and Configuration of the Cloud Management Gateway

Configuration Manager 1610 introduced the cloud management gateway to offer a simpler way to manage your internet-based clients. The cloud management gateway service is deployed to Azure and requires an Azure subscription.

The high-level certificate steps are:

  • Create and issue a custom Web Service Certificate (SSL Cert)
  • Request the Web Service Certificate (SSL Cert) from your CA
  • Export the custom Web Service Certificate (SSL Cert)
  • Create a Client Authentication Certificate
  • Create an Auto-Enroll Group Policy for the Client Authentication Certificate
  • Export the Client Root Certificate (CA / PKI Cert)
  • Upload the Management Cert to Azure

The high-level SCCM console management steps are:

  • Create the Cloud Management Gateway in SCCM
  • Add the Cloud Management Gateway Connector Point role
  • Configure the Management Point for the Cloud Management Gateway
  • Verify the Client is communicating with the Cloud Management Gateway

Requirements

  • Cloud Management Gateway Connection Point role added to Site Server
  • Azure subscription
  • Client Certificate (Management Cert)
  • Web Certificate (SSL Cert)
  • Root Certificate (CA / PKI Cert)

Limitations

  • Each CMG Supports 4000 clients
  • CMG only supports MP and SUP roles
  • No Client Push
  • No OSD or Task Sequences
  • Wake on LAN
  • Peer cache

Understanding the Required Certificates

Web Service Certificate (SSL Cert)

The Web Service Certificate is used by Cloud Management Gateway when authenticating with the clients. It’s recommended that this certificate come from a Public CA and the certificate subject name match the public domain of your company. This certificate will be exported to a file which will be the Management Cert

Management Cert

The Management Certificate is used to authenticate SCCM with Azure and configure and setup the instances of Cloud Management Gateway. After the certificate is created, go ahead and upload the certificate into the Azure portal.

Client Cert

A client cert is required to be on any computer that will be managed by the Cloud Management Gateway. It also needs to be on you site server hosting the Cloud Management Gateway connection point. You can deploy the client certificate to your SCCM clients with an auto-enrollment GPO. Once the Client Cert is on a machine, the Client’s Root Certificate needs to be exported. This will become the Client Root Certificate (CA / PKI Cert)

Client Root Certificate (CA / PKI Cert)

The Root Certificate for the clients PKI certificate. Internet-based clients still require the use of PKI certificates to authenticate with Configuration Manager. This is the root of that PKI certificate.

Create the Cloud Management Gateway in SCCM

Now that you have your certificates created, you need to enable the Cloud Management Gateway feature in the console by going to Administration -> Overview -> Cloud Services -> Updates and Servicing -> Features and right-click to the turn the feature.

Next, go to Administration -> Overview -> Site Configuration -> Sites and set the Client Computer Communication to HTTPS or HTTP and Use PKI Client Cert

Now, we will create the Cloud Management Gateway. Go to Administration -> Overview -> Cloud Services -> Cloud Management Gateway. Click Create Cloud Management Gateway.

Next add your Azure Subscription ID and Upload the Management Cert

On next page we’ll upload the Web Service Certificate (SSL Cert) and the Client Root Certificate (CA / PKI Cert)

Once the wizard finishes, you will see the Cloud Management Gateway Provision and then Complete.

Add the Cloud Management Gateway Connector Point role

Now we need to add the Cloud Management Gateway Connector Point role. Go to Administration -> Overview -> Site Configuration -> Server and Site Roles. Then add the role.

Go through the wizard with the default settings and make sure it chose the Cloud Management Gateway you created earlier.

Configure the Management Point for the Cloud Management Gateway

Now we have to tell the Management Point that is OK to accept Cloud Management Gateway Traffic. Go to Administration -> Overview -> Site Configuration -> Servers and Site Server Roles. Open your Management Point Properties.

Select the settings to Allow Configuration Manager Cloud Management Gateway Traffic.

Verify the Client is communicating with the Cloud Management Gateway

Finally, we need to verify everything is working. Connect one of your clients to an external internet connection such as a home Wi-Fi.

Run a Machine Policy Retrieval & Evaluation cycle from the Configuration Manager app

And finally verify under the Network Tab that you are connected to your Cloud Management Gateway

If you need more help creating certificates or have custom settings you would like to apply to your Cloud Management Gateway, consult the latest Microsoft Documentation for setting up a Cloud Management Gatewy.

Overview

I created this tool, SCCM SUG to Configuration Baseline, to allow you to easily convert an SCCM Software Upgrade Group to a Configuration Baseline. This would most likely be used if you wanted to target a specific Client Setting or Application based on computers which fail compliance for a particular Software Update Group to a collection. Although this might already be possible to do by selecting the software updates within a Software Update Group and creating a Configuration Baseline as a result, this tool can easily automate the process on a schedule in the background.

This tool gathers Software Updates within a Software Update Group via queries to WMI on your SCCM site server, then builds a Configuration Baseline XML and XML Resource File (.RESX) with those items that are used to import back into SCCM. The tool can either compress them as a .CAB file for direct importing via the SCCM Console GUI or import them through a WMI instance POST.

Installing the Tool

Download the required file below and unzip them into a directory of your choosing.

Once downloaded, edit New-DGMSCCMSUGBaseline.ps1 and update the line at the very end of the script to include your SCCM Site Server (ProviderMachineName), your siteCode and the Software Update Group Name (SUGName).


New-DGMSCCMSUGBaseline -ProviderMachineName SCCMSERVER001 -siteCode XXX -SUGName "Software Upgrade Group Name"

You can also specify a FileSavePath which is the location the XML and RESX and CAB files will save if you would like to manipulate them or import them manually.


-fileSavePath "C:\users\username\Desktop\"

Additionally, the tool will require and import the SCCM module \ConfigurationManager.psd1. This module is included when you install the SCCM Console, so typically this script needs to be run on a computer that has the SCCM Console installed, and from an account that can make WMI queries against the SCCM Site Server.

Using the Tool

To start the tool, run the New-DGMSCCMSUGBaseline.ps1 script within PowerShell. Remember to include your desired Software Update Group name as the SUGName argument. You can also pipe the SUGName into the tool by running it as:


"Software Upgrade Group Name" | New-DGMSCCMSUGBaseline -ProviderMachineName SCCMSERVER001 -siteCode XXX

This could be useful if you’d like to automate or iterate through a group of Software Update Groups such as this example which would convert every one of your Software Update Groups to a Configuration Baseline:


$SoftwareUpdateGroups = Get-CMSoftwareUpdateGroup | Select Name

Foreach  ($SoftwareUpdateGroup in $SoftwareUpdateGroups){
   $SoftwareUpdateGroup | New-DGMSCCMSUGBaseline -ProviderMachineName SCCMSERVER001 -siteCode XXX
}

If the tool was successful, you will see it create the importation files in the directory you chose (if none was chosen, look in C:\SUG):

Additionally in SCCM you will see a new Configuration Baseline based off the Software Update Group You Specified:

CB.Software.Update.(SVR – 1 – Pilot Server Updates – Net Framework 2018-02-13 07:50:09)

If a Configuration Baseline already exists you will be prompted if would like to replace it. If you’d like to automatically replacing the baseline without prompting you, replace this if logic within the Get-DGMSCCMWMISUGConfigurationBaselineDetails Function:


#A SUG Baseline Already Exists for a Valid SUG Group Name. Let's determine from the user if this should be replaced
if ((Read-Host $ProviderMachineName ": A Baseline for $SUGName already exists. Do you want to proceed? (Y/N)").Tolower() -eq "n")

With something that will never occur, such as


if ($x = “theskyisblue”)

Understanding the Exported XML

When the tool is run, it will create an XML of the Software Updates that will be imported back into SCCM. This is an XML in the same format that would be inside of a Configuration Baseline CAB file if you to export one from the Console. It can be useful to understand how this file works if you’d like to manipulate the one I create with this tool, or one that is created from a Configuration Baseline export in the console:

PowerShell Function: New-DGMSCCMSUGBaseline.ps1


function New-DGMSCCMSUGBaseline{
<#  
.SYNOPSIS  
    Create a Configuration Baseline Based off a Software Update Group  
.DESCRIPTION  
    Create a Configuration Baseline Based off a Software Update Group 
.NOTES  
    File Name  : New-DGMSCCMSUGBaseline.ps1  
    Author     : David Maiolo - david.maiolo@gmail.com
    Version    : 2018-03-07
.LINK  
#>
 
    param(

        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        $SUGName,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$false)]
        $ProviderMachineName,
        [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$false)]
        $siteCode,
        [Parameter(Position=3,Mandatory=$false,ValueFromPipeline=$false)]
        [string]$fileSavePath = "c:\SUG\"
        

    )

    #Connect to SCCM
    # Import the ConfigurationManager.psd1 module
    $module = "$($ENV:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1"
    if((Get-Module ConfigurationManager) -eq $null) {
        Write-Host Importing $module ...
        Import-Module $module -Force
    }

    # Connect to the site's drive if it is not already present
    if((Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue) -eq $null) {
        $NewPSDrive = New-PSDrive -Name $SiteCode -PSProvider CMSite -Root $ProviderMachineName
    }


    function Get-DGMSUGGroupID{
    <#  
    .SYNOPSIS  
        Query WMI to get Configuration ID of Software Update Group  
    .NOTES  
        File Name  : New-DGMSCCMSUGBaseline.ps1  
        Author     : David Maiolo - david.maiolo@gmail.com
        Version    : 2018-03-07
    .LINK  
    #>
        [CmdletBinding()]
        [Alias()]
        Param
        (
            [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$false)]
            [String]$ProviderMachineName,
            [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$false)]
            [ValidateLength(3,3)]
            [String]$Sitecode,
            [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$false)]
            [String]$SUGName
        )
        

        #Set SCCM WMI NameSpace
        $SCCMnameSpace = "root\SMS\SITE_$siteCode"

        #Query for Software Update Group Information
        $qry = "SELECT CI_ID FROM SMS_AuthorizationList where LocalizedDisplayName = '$SUGName'"

        try{
            $objComputerSystemProduct = Get-WmiObject -ComputerName $ProviderMachineName -Namespace $SCCMnameSpace -Query $qry
            if ($objComputerSystemProduct -eq $null){
                 Write-Host $ProviderMachineName ": An invalid SUG Group name was SUGplied. Exiting." -foregroundcolor red
                 break
            }else{
                #Write-Host $ProviderMachineName ": Succesfully queried WMI for SUG Configuration ID." -foregroundcolor green
                return $objComputerSystemProduct.CI_ID
            }
                
        }catch{
            Write-Host $ProviderMachineName ": Could NOT query WMI for SUG Configuration ID:" ($error[0]) -foregroundcolor red
            break
        }

    }

    function Get-DGMSCCMWMISUGGroupChildren{
    <#  
    .SYNOPSIS  
        Query WMI to get all Software Updates in a Software Updae Group  
    .NOTES  
        File Name  : New-DGMSCCMSUGBaseline.ps1  
        Author     : David Maiolo - david.maiolo@gmail.com
        Version    : 2018-03-07
    .LINK  
    #>
        [CmdletBinding()]
        [Alias()]
        Param
        (
            [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$false)]
            [String]$ProviderMachineName,
            [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$false)]
            [ValidateLength(3,3)]
            [String]$Sitecode,
            [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$false)]
            [String]$SUGConfigurationID
        )
        

        #Set SCCM WMI NameSpace
        $SCCMnameSpace = "root\SMS\SITE_$siteCode"

        #Query for Software Update Group Information
        $qry = "SELECT upd.* FROM SMS_SoftwareUpdate upd, SMS_CIRelation cr WHERE cr.FromCIID= $SUGConfigurationID AND cr.RelationType=1 AND upd.CI_ID=cr.ToCIID"

        try{
            $objComputerSystemProduct = Get-WmiObject -ComputerName $ProviderMachineName -Namespace $SCCMnameSpace -Query $qry
            if ($objComputerSystemProduct.Length -le 0){
                 Write-Host $ProviderMachineName ": An invalid SUG CI ID was SUGplied or no Software Updates exist in the SUG. Exiting." -foregroundcolor red
                 break
            }else{
                #Write-Host $ProviderMachineName ": Succesfully queried WMI for SUG Group Information." -foregroundcolor green
                return $objComputerSystemProduct
            }
                
        }catch{
            Write-Host $ProviderMachineName ": Could NOT query WMI for SUG Group Information:" ($error[0]) -foregroundcolor red
            break
        }

    }

    function Get-DGMSCCMWMISUGConfigurationBaselineDetails{
    <#  
    .SYNOPSIS  
        Query WMI to get Details of a Configuration Baseline  
    .NOTES  
        File Name  : New-DGMSCCMSUGBaseline.ps1  
        Author     : David Maiolo - david.maiolo@gmail.com
        Version    : 2018-03-07
    .LINK  
    #>
        [CmdletBinding()]
        [Alias()]
        Param
        (
            [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$false)]
            [String]$ProviderMachineName,
            [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$false)]
            [ValidateLength(3,3)]
            [String]$Sitecode,
            [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$false)]
            [String]$SUGName,
            [Parameter(Position=3,Mandatory=$true,ValueFromPipeline=$false)]
            $SUGGroupChildren
        )
        
        #Build Baseline Name
        $SUGBaselineNameStart = "CB.Software.Update."
        $SUGBaselineName = $SUGBaselineNameStart + "(" + $SUGName + ")"

        #Set SCCM WMI NameSpace
        $SCCMnameSpace = "root\SMS\SITE_$siteCode"

        #Query for Software Update Group Information
        $qry = "SELECT * FROM SMS_ConfigurationBaselineInfo where LocalizedDisplayName = '$SUGBaselineName'"

        try{
            $objComputerSystemProduct = Get-WmiObject -ComputerName $ProviderMachineName -Namespace $SCCMnameSpace -Query $qry

            if ($objComputerSystemProduct -eq $null){
                #No SUG Baseline Yet Exists for a Valid SUG Group Name. Let's set one up
                Write-Host $ProviderMachineName ": No SUG Baseline Exists Yet for $SUGName. Setting up details for XML..."
                $NewBaseLine = $TRUE
		        $ScopeID = $SUGGroupChildren[0].ModelName.Substring(0,$SUGGroupChildren[0].ModelName.IndexOf("/")) -replace "Site_", "ScopeID_"
		        $BaselineLogicalName = "Baseline_" + [guid]::NewGuid().ToString()
		        $BaselineVersion = 1
            }else{
                #A SUG Baseline Already Exists for a Valid SUG Group Name. Let's determine from the user if this should be replaced
                if ((Read-Host $ProviderMachineName ": A Baseline for $SUGName already exists. Do you want to proceed? (Y/N)").Tolower() -eq "n")
		        {
			        Write-Host $ProviderMachineName ": A duplicate baseline creation has been by the user, exiting without making changes." -ForegroundColor Yellow
			        break
		        }else{
                    $BaselineCI_ID = $objComputerSystemProduct.CI_ID 
		            $BaselineCI_UniqueID = $objComputerSystemProduct.CI_UniqueID
		            
                    $NewBaseLine = $FALSE
                    $ScopeID = $BaselineCI_UniqueID.substring(0,$BaselineCI_UniqueID.indexof("/"))
		            $BaselineLogicalName = $objComputerSystemProduct.CI_UniqueID.substring($objComputerSystemProduct.CI_UniqueID.indexof("/")+1)
		            $BaselineVersion = $objComputerSystemProduct.SDMPackageVersion + 1


                     #Query for CI Information
                    $qry = "SELECT * FROM SMS_ConfigurationItem where CI_ID = $BaselineCI_ID"

                    try{
                        $CI = Get-WmiObject -ComputerName $ProviderMachineName -Namespace $SCCMnameSpace -Query $qry
                        if ($CI -eq $null){
                             Write-Host $ProviderMachineName ": CI $BaselineCI_ID does not exist, no action taken." -foregroundcolor red
                             break
                        }
                    }
                    catch{
                        Write-Host $ProviderMachineName ": Could NOT query WMI for CI ID $BaselineCI_ID :" ($error[0]) -foregroundcolor red
                        break
                    }
                    
                }
            }
            

            $result = [PSCustomObject] @{
                'NewBaseLine' = $NewBaseLine;
                'SUGBaselineName' = $SUGBaselineName;
                'ScopeID' = $ScopeID;
                'BaselineLogicalName' = $BaselineLogicalName;
                'BaselineVersion' = $BaselineVersion;
                'CI' = $CI;
            }

            return $result
               
        }catch{
            Write-Host $ProviderMachineName ": Could NOT query WMI for SUG Baseline:" ($error[0]) -foregroundcolor red
            break
        }

    }

    function New-DGMSCCMWMISUGConfigurationBaselineXML{
    <#  
    .SYNOPSIS  
        Create a new XML formatted file that will be used as a Configuration Baseline Import
    .NOTES  
        File Name  : New-DGMSCCMSUGBaseline.ps1  
        Author     : David Maiolo - david.maiolo@gmail.com
        Version    : 2018-03-07
    .LINK  
    #>
        [CmdletBinding()]
        [Alias()]
        Param
        (
            [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$false)]
            [String]$ProviderMachineName,
            [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$false)]
            [ValidateLength(3,3)]
            [String]$Sitecode,
            [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$false)]
            $SUGConfigurationBaselineDetails,
            [Parameter(Position=3,Mandatory=$true,ValueFromPipeline=$false)]
            $SUGGroupChildren


        )

        try{

            $SUGBaselineName = $SUGConfigurationBaselineDetails.SUGBaselineName
            $ScopeID = $SUGConfigurationBaselineDetails.ScopeID
            $BaselineLogicalName = $SUGConfigurationBaselineDetails.BaselineLogicalName
            $BaselineVersion = $SUGConfigurationBaselineDetails.BaselineVersion

            $baselineXML = @"


  
  
  
    
      
      
    
    
    
    
    
    

"@

	foreach($SUGGroupChild in $SUGGroupChildren)
	{
		$ModelName = $SUGGroupChild.ModelName.Substring(0,$SUGGroupChildren[0].ModelName.IndexOf("/"))
		$LogicalName = $SUGGroupChild.ModelName.Substring($SUGGroupChildren[0].ModelName.IndexOf("/")+1)
		
        $baselineXML += @"
      

"@
	}

	    $baselineXML += @"
    
    
    
  

"@
        return $baselineXML
    }
    catch{
        Write-Host $ProviderMachineName ": Could NOT generate a Baseline XML based off the data provided:" ($error[0]) -foregroundcolor red
    }

    }

    function Get-DGMSCCMSUGXMLResource{
    <#  
    .SYNOPSIS  
        Create a new XML formatted resource file that will be used as a Configuration Baseline Import
    .NOTES  
        File Name  : New-DGMSCCMSUGBaseline.ps1  
        Author     : David Maiolo - david.maiolo@gmail.com
        Version    : 2018-03-07
    .LINK  
    #>
    [CmdletBinding()]
        [Alias()]
        Param
        (
            [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$false)]
            $SUGConfigurationBaselineDetails
        )

        try{

            $SUGBaselineName = $SUGConfigurationBaselineDetails.SUGBaselineName
            $ScopeID = $SUGConfigurationBaselineDetails.ScopeID
            $BaselineLogicalName = $SUGConfigurationBaselineDetails.BaselineLogicalName
            $BaselineVersion = $SUGConfigurationBaselineDetails.BaselineVersion

            $ScopeID = $ScopeID -replace "Scope",""

            $SUGResourceXML = @"


  
  
    
    
      
        
          
            
              
                
              
              
              
              
              
            
          
          
            
              
              
            
          
          
            
              
                
                
              
              
              
              
              
            
          
          
            
              
                
              
              
            
          
        
      
    
  
  
    text/microsoft-resx
  
  
    2.0
  
  
    System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  
  
    System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
  
  
    $SUGBaselineName
  

"@

            return $SUGResourceXML
        }
        catch{
            Write-Host $ProviderMachineName ": Could NOT create XML Resource File:" ($error[0]) -foregroundcolor red
        }

    }

    function Import-DGMSCCMWMISUGConfigurationBaselineXML{
    <#  
    .SYNOPSIS  
        Import an XML formatted file and resource file that will be become a Configuration Baseline
    .NOTES  
        File Name  : New-DGMSCCMSUGBaseline.ps1  
        Author     : David Maiolo - david.maiolo@gmail.com
        Version    : 2018-03-07
    .LINK  
    #>
        [CmdletBinding()]
        [Alias()]
        Param
        (
            [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$false)]
            [String]$ProviderMachineName,
            [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$false)]
            [ValidateLength(3,3)]
            [String]$siteCode,
            [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$false)]
            $DGMSCCMSUGXMLResource,
            [Parameter(Position=3,Mandatory=$true,ValueFromPipeline=$false)]
            $SUGConfigurationBaselineXML,
            [Parameter(Position=4,Mandatory=$true,ValueFromPipeline=$false)]
            $SUGConfigurationBaselineDetails
        )

        
        try{

            $NewBaseLine = $SUGConfigurationBaselineDetails.NewBaseLine
            $SUGBaselineName = $SUGConfigurationBaselineDetails.SUGBaselineName
            $ScopeID = $SUGConfigurationBaselineDetails.ScopeID
            $BaselineLogicalName = $SUGConfigurationBaselineDetails.BaselineLogicalName
            $BaselineVersion = $SUGConfigurationBaselineDetails.BaselineVersion

	        if ($NewBaseLine -eq $TRUE)
	        {
                Write-Host $ProviderMachineName ": Building Details for New Baseline: $SUGBaselineName..."


                 #Query for CI Information

                $LD = [PSCustomObject] @{
                'LocaleID' = 1033;
                'LocalizedData' = $DGMSCCMSUGXMLResource;
                }


                $CI = [PSCustomObject] @{
                    'SDMPackageLocalizedData' = $LD;
                    'IsBundle' = $false;
                    'IsExpired' = $false;
                    'IsUserDefined' = $true
                    'ModelID' = 16777367
                    'PermittedUses' = 0
                    'PlatformCategoryInstance_UniqueIDs' = "Platform:C92857DF-9FD1-4FAD-BAA1-BE9FAD4B4F74"
                    'SDMPackageXML' = $SUGConfigurationBaselineXML;
                }

	        }else{
                $CI = $SUGConfigurationBaselineDetails.CI

                Write-Host $ProviderMachineName ": Building Details for Pre-Existing Baseline: $SUGBaselineName..."
            }
	
	        if ($NewBaseLine -eq $FALSE) {
                Write-Host $ProviderMachineName ": Creating baseline..."
                $NameSpace = "root\SMS\SITE_$siteCode"
                Set-WmiInstance -ComputerName $ProviderMachineName -Namespace $NameSpace -Class SMS_ConfigurationItem -PutType Create -Argument $CI
            }else {
                Write-Host $ProviderMachineName ": Updating baseline..."
                $NameSpace = "root\SMS\SITE_$siteCode"
                Set-WmiInstance -ComputerName $ProviderMachineName -Namespace $NameSpace -Class SMS_ConfigurationItem -PutType UpdateOnly -Argument $CI
            }

            Write-Host $ProviderMachineName ": Baseline Import Succesful: $SUGBaselineName" -ForegroundColor Green
        }
        catch{
            Write-Host $ProviderMachineName ": Could NOT import XML data or XML Resource data for Baseline $SUGBaselineName into SCCM:" ($error[0]) -foregroundcolor red
        }
    }

    function New-CabinetFile {
    <#  
    .SYNOPSIS  
        Create a cabinet file using a list of files as the source. This is used best for importing into SCCM
    .NOTES  
        File Name  : New-DGMSCCMSUGBaseline.ps1  
        Author     : David Maiolo - david.maiolo@gmail.com
        Version    : 2018-03-07
    .LINK  
    #>
        [CmdletBinding()]
        Param(
            [Parameter(HelpMessage="Target .CAB file name.", Position=0, Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
            [ValidateNotNullOrEmpty()]
            [Alias("FilePath")]
            [string] $Name,
 
            [Parameter(HelpMessage="File(s) to add to the .CAB.", Position=1, Mandatory=$true, ValueFromPipeline=$true)]
            [ValidateNotNullOrEmpty()]
            [Alias("FullName")]
            [string[]] $File,
 
            [Parameter(HelpMessage="Default intput/output path.", Position=2, ValueFromPipelineByPropertyName=$true)]
            [AllowNull()]
            [string[]] $DestinationPath,
 
            [Parameter(HelpMessage="Do not overwrite any existing .cab file.")]
            [Switch] $NoClobber
            )
 
        Begin { 
    
            ## If $DestinationPath is blank, use the current directory by default
            if ($DestinationPath -eq $null) { $DestinationPath = (Get-Location).Path; }
            Write-Verbose "New-CabinetFile using default path '$DestinationPath'.";
            Write-Verbose "Creating target cabinet file '$(Join-Path $DestinationPath $Name)'.";
 
            ## Test the -NoClobber switch
            if ($NoClobber) {
                ## If file already exists then throw a terminating error
                if (Test-Path -Path (Join-Path $DestinationPath $Name)) { throw "Output file '$(Join-Path $DestinationPath $Name)' already exists."; }
            }
 
            ## Cab files require a directive file, see 'http://msdn.microsoft.com/en-us/library/bb417343.aspx#dir_file_syntax' for more info
            $ddf = ";*** MakeCAB Directive file`r`n";
            $ddf += ";`r`n";
            $ddf += ".OPTION EXPLICIT`r`n";
            $ddf += ".Set CabinetNameTemplate=$Name`r`n";
            $ddf += ".Set DiskDirectory1=$DestinationPath`r`n";
            $ddf += ".Set MaxDiskSize=0`r`n";
            $ddf += ".Set Cabinet=on`r`n";
            $ddf += ".Set Compress=on`r`n";
            ## Redirect the auto-generated Setup.rpt and Setup.inf files to the temp directory
            $ddf += ".Set RptFileName=$(Join-Path $ENV:TEMP "setup.rpt")`r`n";
            $ddf += ".Set InfFileName=$(Join-Path $ENV:TEMP "setup.inf")`r`n";
 
            ## If -Verbose, echo the directive file
            if ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) {
                foreach ($ddfLine in $ddf -split [Environment]::NewLine) {
                    Write-Verbose $ddfLine;
                }
            }
        }
 
        Process {
   
            ## Enumerate all the files add to the cabinet directive file
            foreach ($fileToAdd in $File) {
        
                ## Test whether the file is valid as given and is not a directory
                if (Test-Path $fileToAdd -PathType Leaf) {
                    Write-Verbose """$fileToAdd""";
                    $ddf += """$fileToAdd""`r`n";
                }
                ## If not, try joining the $File with the (default) $DestinationPath
                elseif (Test-Path (Join-Path $DestinationPath $fileToAdd) -PathType Leaf) {
                    Write-Verbose """$(Join-Path $DestinationPath $fileToAdd)""";
                    $ddf += """$(Join-Path $DestinationPath $fileToAdd)""`r`n";
                }
                else { Write-Warning "File '$fileToAdd' is an invalid file or container object and has been ignored."; }
            }       
        }
 
        End {
    
            $ddfFile = Join-Path $DestinationPath "$Name.ddf";
            $ddf | Out-File $ddfFile -Encoding ascii | Out-Null;
 
            Write-Verbose "Launching 'MakeCab /f ""$ddfFile""'.";
            $makeCab = Invoke-Expression "MakeCab /F ""$ddfFile""";
 
            ## If Verbose, echo the MakeCab response/output
            if ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) {
                ## Recreate the output as Verbose output
                foreach ($line in $makeCab -split [environment]::NewLine) {
                    if ($line.Contains("ERROR:")) { throw $line; }
                    else { Write-Verbose $line; }
                }
            }
 
            ## Delete the temporary .ddf file
            Write-Verbose "Deleting the directive file '$ddfFile'.";
            Remove-Item $ddfFile;
 
            ## Return the newly created .CAB FileInfo object to the pipeline
            Get-Item (Join-Path $DestinationPath $Name);
        }
    }


    $DGMSUGGroupID = Get-DGMSUGGroupID -ProviderMachineName $ProviderMachineName -Sitecode $siteCode -SUGName $SUGName
    $DGMSCCMWMISUGGroupChildren = Get-DGMSCCMWMISUGGroupChildren -ProviderMachineName $ProviderMachineName -Sitecode $siteCode -SUGConfigurationID $DGMSUGGroupID
    $DGMSCCMWMISUGConfigurationBaselineDetails = Get-DGMSCCMWMISUGConfigurationBaselineDetails -Sitecode $siteCode -SUGName $SUGName -ProviderMachineName $ProviderMachineName -SUGGroupChildren $DGMSCCMWMISUGGroupChildren
    $DGMSCCMSUGXMLResource = Get-DGMSCCMSUGXMLResource -SUGConfigurationBaselineDetails $DGMSCCMWMISUGConfigurationBaselineDetails
    $DGMSCCMWMISUGConfigurationBaselineXML = New-DGMSCCMWMISUGConfigurationBaselineXML -ProviderMachineName $ProviderMachineName -Sitecode $siteCode -SUGConfigurationBaselineDetails $DGMSCCMWMISUGConfigurationBaselineDetails -SUGGroupChildren $DGMSCCMWMISUGGroupChildren
    

    #Create Resource Files and Cab Files for Import

    $filePath = "c:\SUG\"

    $SUGGroupFileName = $SUGName -replace '[^a-zA-Z0-9]', ''

    $XMLFile = "$SUGGroupFileName.xml"
    $XMLResourceFile = "$SUGGroupFileName.resx"
    $CabinetFile = "$SUGGroupFileName.cab"

    $XMLFilePath = Join-Path $filePath $XMLFile
    $XMLResourceFilePath = Join-Path $filePath $XMLResourceFile
    $CabinetFilePath = Join-Path $filePath $CabinetFile

    $DGMSCCMWMISUGConfigurationBaselineXML | Out-File -FilePath $XMLFilePath
    $DGMSCCMSUGXMLResource | Out-File -FilePath $XMLResourceFilePath

    New-CabinetFile -Name $CabinetFile -File $XMLFilePath,$XMLResourceFilePath -DestinationPath $filePath

    #Import-DGMSCCMWMISUGConfigurationBaselineXML -ProviderMachineName $ProviderMachineName -siteCode $siteCode -DGMSCCMSUGXMLResource $DGMSCCMSUGXMLResource -SUGConfigurationBaselineXML $DGMSCCMWMISUGConfigurationBaselineXML -SUGConfigurationBaselineDetails $DGMSCCMWMISUGConfigurationBaselineDetails
    
    # Set the current location to be the site code and import the baseline
    Set-Location "$($SiteCode):\"
    Import-CMBaseline -FileName $CabinetFilePath -Force

    $XMLFile
    $XMLResourceFile
    $CabinetFile
}


New-DGMSCCMSUGBaseline -ProviderMachineName SCCMSERVER001 -siteCode XXX -SUGName "Software Upgrade Group Name" -fileSavePath "C:\users\you\Desktop\"

Overview

I developed this tool, Run-DGMFireEyeHXCompliance.psm1, to test and confirm a FireEye Endpoint Security (HX) rollout in a corporate environment. Additionally, at the end of this document I have provided you with a FireEye HX Deployment Strategy approach for your corporate environment.

For some background, FireEye Endpoint Security (HX) is an Endpoint Forensics product provided by FireEye and is part of the Endpoint Security (HX Series) of 5th Generation Appliances. It is an endpoint security tool to help an organization monitor indicators of compromise (IOC) on endpoints and respond to cyber-attacks on the endpoint before data loss might occur.

DGMFireEyeHXCompliance Tool

The DGMFireEyeHXCompliance tool remotely invokes a test routine to gather data from an HX endpoint perspective and is intended to be deployed after an HX cloud rollout. Once deployed, it performs the following tasks on your HX endpoint computers:

  • Egress access on port 443 is open to the FireEye HX Cloud Connector
  • Egress access on port 80 is open to the FireEye HX Cloud Connector
  • The FireEye HX xagt service is able to start properly.
  • After a specified wait time, the xagt service is continuing to run. This is to test for incompatibilities with the service on certain workstations.
  • Start a Microsoft Message Analyzer packet capture (Primary Event Trace Log (ETL)) for a specific period of time, restarting the FireEye HX xagt service at the moment of the Primary Event Trace Log (ETL), if stopped.
  • Stop and save Primary Event Trace Log (ETL) results along with packet capture help cab files.
  • Output any Web Proxy PAC configuration file URLs, if configured
  • Generate a FireEye HX troubleshooting log file and save
  • Combine packet capture results, packet capture assist files and xagt troubleshooting log files to a central location of your choosing.
  • Continue testing of the next FireEye HX endpoint computer supplied in the array

The tool runs the test results on each HX endpoint, and aggregates the following results centrally:

  • Test information as shown above during the test routine
  • Microsoft Message Analyzer CAB File (containing many Network Analysis Results inside)
  • Microsoft Message Analyzer Packet Capture Session File (Primary Event Trace Log (ETL))
  • xagt Service Troubleshooting Log

Downloading the DGMFireEyeHXCompliance Tool

To download the tool, download and extract the module below. Inside you will find the single invocation and application module Run-DGMFireEyeHXCompliance.psm1.

Running the DGMFireEyeHXCompliance Tool

Before running the tool, update the values within the Invoke-DGMFireEyeHXCompliance function at the end of the script. First, change the save path you would like the logs and trace results saved. This is the local path on the computer that will initiate the script:


$LocalPath = "c:\Run-DGMFireEyeHXCompliance\" 

Next, indicate where you would like the results saved on the HX endpoint computers. These results will be removed automatically once aggregated to the source computer:


$RemotePath = "c:\temp\"

Finally, update the $HXEndpointComputers array to indicate the list of HX endpoint computers you would like to run the test against. You can also pass an array of computers to the application through the pipeline.


$HXEndpointComputers = @("DGM-SITE01-TST","DGM-SITE02-TST","VDGMDFS005VER","VDGMDFS006VER","WORKSTATION001","WORKSTATION002","WORKSTATION003")

Additionally, you will need to update the FireEye HX Cloud Connector address to match the connector in your environment, and additionally any ports you may wish to test. You can find this line at the bottom of the Run-DGMFireEyeHXCompliance function:


Get-DGMTCPPortOpenResults -tcpports 80,443 -destinationAddress "dgmtest-hxdmz-agent-1.helix.apps.fireeye.com"

To run the application, load the module Run-DGMFireEyeHXCompliance.psm1 in PowerShell ISE and run the invocation function. ***Make sure the PowerShell session is run with an account that has full administrative permissions to each of the endpoint computers*** Failure to do this, will produce errros when attempting to invoke a remote PowerShell session or collect test results.


Invoke-DGMFireEyeHXCompliance

As the tool runs, each of the HX endpoints will loop through the test items, aggregating results to the central source.

Understanding DGMFireEyeHXCompliance Test Results

Once the tool has completed, first review the output as it contains some test results that can only be viewed within the console. A sample output set is below. We can see in these results the Cloud Connector can be accessed on 80 and 443, but also we see the FireEye HX xagt service has abruptly stopped after starting it and waiting 8 seconds to see if it is still running. This is a legitimate issue that you may notice on Virtual Machines, and subsequent analysis of packet trace data and xagt log results are necessary.

The tool outputs the log and packet capture results to the directory you choose, and separates the results by sub directory of the HX endpoints you specified within the $HXEndpointComputers variable:

FireEye Packet Trace Analysis with DGMFireEyeHXCompliance

Within each directory, you will find the Microsoft Message Analyzer CAB File, the Microsoft Message Analyzer Packet Capture Session File, and the xagt Service Troubleshooting Log:

To view the Microsoft Message Analyzer Packet Capture Session File, download Microsoft Message Analyzer from https://www.microsoft.com/en-us/download/confirmation.aspx?id=44226. Open the Primary Event Trace Log (ETL) File in Microsoft Message Analyzer. You can opt to filter by TCP, HTTPS, etc to look into possible network issues between the HX endpoint and the cloud connector.

FireEye Advanced Network Analysis with DGMFireEyeHXCompliance

To view the Microsoft Message Analyzer CAB File, extract the CAB contents. Once extracted, you will notice several files, including another copy of the Primary Event Trace Log (ETL) file.

Below are descriptions of each of the files. Use these to diagnose network related issues.

General Configuration

OS Information

osinfo.txt

Credential Providers

allcred.reg.txt

Network Configuration

Environment Information

envinfo.txt

Adapter Information

adapterinfo.txt

DNS Information

dns.txt

Neighbor Information

neighbors.txt

Wireless Configuration

WLAN Auto-Config Eventlog

WLANAutoConfigLog.evtx

Windows Connect Now

WCN Information

WCNInfo.txt

Windows Firewall

Windows Firewall Configuration

WindowsFirewallConfig.txt

Windows Firewall Effective Rules

WindowsFirewallEffectiveRules.txt

Connection Security Eventlog

WindowsFirewallConsecLog.evtx

Connection Security Eventlog (Verbose)

WindowsFirewallConsecLogVerbose.evtx

Firewall Eventlog

WindowsFirewallLog.evtx

Firewall Eventlog (Verbose)

WindowsFirewallLogVerbose.evtx

Winsock Configuration

Winsock LSP Catalog

WinsockCatalog.txt

File Sharing

File Sharing Configuration

filesharing.txt

Registry Key Dumps

Credential Providers

AllCred.reg.txt

Credential Provider Filters

AllCredFilter.reg.txt

API Permissions

APIPerm.reg.txt

WlanSvc HKLM Dump

HKLMWlanSvc.reg.txt

WinLogon Notification Subscribers

Notif.reg.txt

Network Profiles

NetworkProfiles.reg.txt

Trace Files

Primary Event Trace Log (ETL)

report.etl

FireEye XAGT Service Log Analysis with DGMFireEyeHXCompliance

Included with the compliance analysis is the xagt service troubleshooting log file. This was gathered by running C:\Program Files (x86)\FireEye\xagt\xagt.exe -g <logFilePath> locally on each HX endpoint.

Pay specific attention towards the end of the file, looking for possible certificate issues, etc. As we see in this example, there is an error verifying the CRL signature in the .\crypto\rsa\rsa_pk1 file.

FireEye HX Deployment Approach

Purpose

The purpose of this section is to help you define a deployment strategy and plan for a FireEye HX Cloud deployment in your corporate environment. This section is comprised of two sections: the Deployment Strategy and the Deployment Plan. The Deployment Strategy section is used to formulate a deployment approach for FireEye HX Cloud (xAgt 26.21.8). The Deployment Plan section contains detailed schedule recommendations, resource, technical, and support information necessary for a successful deployment of the FireEye HX Cloud (xAgt 26.21.8).

RECOMMENDED SETTINGS For Deployment

The following are recommended configurations for your JSON file version 26.21.8. Note a deployment of the xAgent installation will either update the previous version of the xAgent or install the new version of xAgent if it does not already contain it.

  • Server: URL of FireEye HX Cloud Connector
  • Active Collection Enabled: False
  • Production Exploit Detection Enabled: False
  • FIPS Enabled: False
  • Configuration Poll Interval: 900 Seconds
  • CPU Limit: 50%
  • Protection Services Enabled: True
  • Age to Purge Protection Services: 90 Days

Deployment Strategy

The Deployment Strategy section of this article provides an overview of the deployment strategy you should plan for a FireEye HX Cloud (xAgt 26.21.8) rollout. Included in the deployment strategy is suggested timeline information, a description of the deployment approach, and associated benefits, assumptions and risks.

Deployment Overview

The Deployment Date’s referenced below are the date FireEye HX Cloud (xAgt 26.21.8) could potentially attempt to begin installation on your selected HX endpoint computers. Because the installation of the MSI was designed to be able to occur outside your maintenance window, installation during this phase could be scheduled to occur during any time during your chosen deployment deployment date.

Phases

Sites

Computers

Scheduled Dates

PRE-PILOT

Select Workstations and Servers

10

February 26, 2020 – February 27, 2020

PILOT

Pilot Workstation/Server Group

100

February 28, 2020 – March 02, 2020

PRODUCTION 1A WORKSTATIONS

Production Workstations Group

1,000

TBD Based on Pilot and Pre-Pilot Results

PRODUCTION 1B SERVERS

Production Server Group

400

TBD Based on Pilot and Pre-Pilot Results

Exclusions to Upgrade

The following types of systems should not be targeted within the scope of the project:

  • Citrix CEND Computers (but not CAU or CVU)
  • Domain Controllers (ADC)
  • Domain Joined Appliances
  • Virtual CNOs
  • Systems with no SCCM client

Deployment Plan

Deployment Approach

System Center Configuration Manager (SCCM) should be used to deploy FireEye HX Cloud (xAgt 26.21.8). When each phase is approached, the computers should be instructed to execute the installation in Parallel outside of the maintenance window. Create an SCCM application and deploy it as a required application to your HX endpoints as I have here.

Assumptions and Risks

Assumptions

The workstations and servers targeted for deployment are assumed to be left on and connected to your corporate network during their respective phase window.

Risks

During a deployment of the 24.9.0 version of the xAgent deployment, you may run in to a few issues where processor utilization can heighten during an xAgent scan.

Engagement and Promotion Strategy

During each deployment phase a corporate email should be sent to communicate the associated deployment phases. Members in your teams may choose to notify specific application owners if they feel the need.

Testing Methods and Monitoring

In the event an issue is determined, a rollback to a previous version should be deployed through the uninstall command on the application. Additional information should be available once the Pre-Pilot and Pilot phase of your rollout to xAgent 26.21.8 have completed.

Monitoring The Deployment

Basic Monitoring

Central monitoring of the FireEye HX Cloud (xAgt 26.21.8) rollout can be viewed from your computer by visiting http://sccmserver/Reports/ and searching for the report ‘All application deployments (basic)’.

Choose By: Application

Select Application:

  • FireEye HX Cloud (xAgt 26.21.8) choose your xAgt deployment

Select Collection (Application): All

Clicking the “View Current” data for the phase will allow you to further drill down, even to the computer and user level if necessary:

The SCCM monitoring should be setup by determining your MSI product code associated to the FireEye HX Cloud (xAgt 26.21.8) MSI installer exists on the machine. In this example, the product code is:

{CB3A0A18-EA4B-45AA-801B-AAA9D00CABE5}

Critical-Need Endpoint Strategy

I developed an additional solution that can allow you to start or stop the Mandiant xAgt on any set of your HX endpoints if they are a critical-need system where you do not want the agent running during a certain time span.

Script 1: A small program that takes a list of your HX endpoint critical-need computers as input, and pauses the Mandiant agent on these computers.

Script 2: Another small program that resumes the Mandiant agent, again taking the same list as input.

The first script should be run on a schedule as a scheduled task. In this example, we choose 7AM, and then then the second program at 8PM. This creates a “Mandiant Agent is off during business hours and on at night” environment.

cid:image004.png@01D34BEE.C3FEED40

Technical Notes on Critical-Need Endpoint Computers Start / Stop Procedure

STOP / SET TO MANUAL THE xagt SERVICE ON Critical-Need MACHINES
  1. RDP into the server you want the scheduled task to run on
  2. Open PowerShell ISE -> C:\Scripts\XAGT_Service_Tasks\XAGT_Service_Tasks_Stop_Service.ps1
  3. Click Run (the Green Play Button)
START / SET TO AUTOMATIC THE xagt SERVICE ON Critical-Need MACHINES
  1. RDP into the server you want the scheduled task to run on
  2. Open PowerShell ISE -> C:\Scripts\XAGT_Service_Tasks\ XAGT_Service_Tasks_Start_Service.ps1
  3. Click Run (the Green Play Button)
MODIFYING LIST of Critical-Need MACHINES

Modify: C:\Scripts\XAGT_Service_Tasks\hostnames_PRODUCTION.txt

LOGS OF YOUR RESULTS

Starting Log: “C:\Scripts\XAGT_Service_Tasks\XAGT_Service_Tasks_START_DATE.log”

Stopping Log: “C:\Scripts\XAGT_Service_Tasks\XAGT_Service_Tasks_STOP_DATE.log”

Mandiant_xAgt_Tasks_Stop_Service.ps1

$path = "C:\Scripts\Mandiant_xAgr_Tasks"
$hostname = Get-Content "$path\hostnames_PRODUCTION.txt"
$service = "xagt"

foreach ($h in $hostname){

    try{
        (get-service -ComputerName $h -Name $service).Stop()
        set-service -ComputerName $h -Name $service -startuptype "manual"
        Write-Host "Success: Stopped and set to Manual xagt service on $h ..." -foregroundcolor green
        Write-Output "Success: $(Get-Date); Stopped and Set to Manual the $service service on $h" >> "$path\Mandiant_xAgt_Tasks_STOP_$(Get-Date -Format dd-MM-yyyy).log"
    }
    catch{
        Write-Host "Error: Could NOT Stop nor set to Manual xagt service on $h ..." -foregroundcolor red
        Write-Output "Error: $(Get-Date); Could NOT Stop nor set to Manual xagt service on $h" >> "$path\Mandiant_xAgt_Tasks_STOP_$(Get-Date -Format dd-MM-yyyy).log"
    }

}
Mandiant_xAgt_Tasks_Start_Service.ps1

$path = "C:\Scripts\Mandiant_xAgr_Tasks"
$hostname = Get-Content "$path\hostnames_PRODUCTION.txt"
$service = "xagt"

foreach ($h in $hostname){

    try{
        (get-service -ComputerName $h -Name $service).Start()
        set-service -ComputerName $h -Name $service -startuptype "Automatic"
        Write-Host "Success: Started and set to Automatic xagt service on $h ..." -foregroundcolor green
        Write-Output "Success: $(Get-Date); Started and Set to Automatic the $service service on $h" >> "$path\Mandiant_xAgt_Tasks_START_$(Get-Date -Format dd-MM-yyyy).log"
    }
    catch{
        Write-Host "Error: Could NOT Start nor set to manual xagt service on $h ..." -foregroundcolor red
        Write-Output "Error: $(Get-Date); Could NOT Start nor set to Automatic xagt service on $h" >> "$path\Mandiant_xAgt_Tasks_START_$(Get-Date -Format dd-MM-yyyy).log"
    }

}

Mandiant Endpoint xAgent Antivirus Exclusion Policies

In order to prevent potential conflicts with antivirus and/or host-based intrusion detection software, a series of files should be whitelisted using SCCM Antimalware exclusion policies. Below are example policies you can create

  • EP -SERVER – Mandiant Endpoint xAgent Antimalware Policy
  • EP -WORKSTATION – Mandiant Endpoint xAgent Antimalware

Collectively, the policies should target the workstations and servers that the Mandiant Endpoint xAgent Application should be deployed to. These policies need to include exclude the following files:

C:\Program Files (x86)\FireEye\xagt\audits.dll

C:\ProgramData\FireEye\xagt\events.db

C:\ProgramData\FireEye\xagt\events.db-wal

C:\Windows\System32\drivers\FeKern.sys

C:\ProgramData\FireEye\xagt\main.db

C:\Program Files (x86)\FireEye\xagt\mindexer.sys

C:\Windows\FireEye\NamespaceToEvents_xx.dll (note the xx is wildcard)

C:\Windows\FireEye\NamespaceToEvents32_xx.dll (note the xx is wildcard)

C:\Program Files (x86)\FireEye\xagt\xagt.exe

C:\ProgramData\FireEye\xagt\xlog.db

C:\ProgramData\FireEye\xagt\xlog.db-wal

The DGMFireEyeHXCompliance PowerShell Module


function Run-DGMFireEyeHXCompliance{
<#  
.SYNOPSIS  
    Test FireEye HX Compliance And Aggregaget Test Resullts   
.NOTES  
    File Name  : Run-DGMFireEyeHXCompliance.psm1  
    Author     : David Maiolo
    Version    : 2018-03-06
.LINK  
#>
 
    param(

        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        $HXEndpointComputers,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$false)]
        $RemotePath,
        [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$false)]
        $LocalPath

    )

 
    #Create a script block to run on the remote machine to test for FireEye issues     
    $script = {
        #define local hostname
        $hostname = $env:COMPUTERNAME
        
        function Get-DGMTCPPortOpenResults{
            <#  
            .SYNOPSIS  
                Test TCP Port and Display Results  
            .NOTES 
                Author     : David Maiolo - david.maiolo@gmail.com  
            .LINK  
            #>
            param(
                $tcpports,
                $destinationAddress
            )
 
            $hostname = $env:COMPUTERNAME
 
            foreach ($port in $tcpports)
            {
       
                try{
                    $Socket = New-Object Net.Sockets.TcpClient      
                    $ErrorActionPreference = 'SilentlyContinue'
       
       
                    $Socket.Connect($destinationAddress, $port)
 
                    if ($Socket.Connected) {
                        Write-Host $hostname ": Outbound port $port is open to $destinationAddress." -ForegroundColor Green
                        $Socket.Close() 
                    }
                    else{
                        Write-Host $hostname ": Outbound port $port is closed or filtered to $destinationAddress." -ForegroundColor Red
                    }
 
                }catch{
                    Write-Host $hostname ": Could NOT open the socket for $port port, so result unknown." -foregroundcolor yellow
                }  
            }
        }
 
        function Start-DGMService{
            <#  
            .SYNOPSIS  
                Attempt to start a Service and Display Results  
            .NOTES 
                Author: David Maiolo - david.maiolo@gmail.com  
            .LINK  
            #>
            [CmdletBinding()]
            [OutputType([int])]
            Param
            (
                [Parameter(Mandatory=$true,ValueFromPipeline=$true, Position=0)]
                $service
            )
 
            $hostname = $env:COMPUTERNAME
            try{
                #(get-service -Name $service).Start()
                #set-service -Name $service -startuptype "Automatic" -ErrorAction Stop
                Start-Service $service -ErrorAction stop
                Write-Host $hostname ": Successfully started the $service service." -foregroundcolor green
            }
            catch{
                Write-Host $hostname ": Could NOT Start $service service:" ($error[0]) -foregroundcolor red
            }
        }
 
       function Test-DGMService{
           <#  
           .SYNOPSIS  
               Look to see if a service is running, stopped, etc and display results  
           .NOTES 
               Author: David Maiolo - david.maiolo@gmail.com  
           .LINK  
           #>
           [CmdletBinding()]
           [OutputType([int])]
           Param
           (
               [Parameter(Mandatory=$true,ValueFromPipeline=$true, Position=0)]
               $service
           )
 
           $hostname = $env:COMPUTERNAME
           try{
               $serviceStatus = (get-service -Name $service).Status
               if (($serviceStatus -eq "Running")){
                   Write-Host $hostname ": The $service service is currently $serviceStatus." -ForegroundColor Green
               }else{
                   Write-Host $hostname ": The $service service is currently $serviceStatus." -ForegroundColor Red
               }
           }
           catch{
               Write-Host $hostname ": The $service service could not be checked for current status. Result unknown." -foregroundcolor yellow
           }
       }

        function Get-DGMProxyPACFile{
        <#  
        .SYNOPSIS  
            Resturn proxy PAC file URL from registry entry  
        .NOTES 
            Author: David Maiolo - david.maiolo@gmail.com  
        .LINK  
        #>
 
            $hostname = $env:COMPUTERNAME
            try{
                $proxyPAC = Get-ItemProperty ('Registry::HKLM\SYSTEM\CurrentControlSet\Services\NlaSvc\Parameters\Internet\ManualProxies')
                $proxyPAC = $proxyPAC.'(default)'
                Write-Host $hostname ": The proxy PAC auto-config file is $proxyPAC."
            }
            catch{
                Write-Host $hostname ": Unable to determine the proxy PAC auto-config file." -foregroundcolor yellow
            }
        }

        function Get-DGMFireEyeLogFile{
            <#  
            .SYNOPSIS  
                Creates xagt log file  
            .NOTES 
                Author: David Maiolo - david.maiolo@gmail.com  
            .LINK  
            #>
            [CmdletBinding()]
            [OutputType([int])]
            Param
            (
            [string]$logFile
            )
 
            $hostname = $env:COMPUTERNAME
            try{

                $logFile = "$hostname`_xagt_$(get-date -f yyyy-MM-ddTHHmmss).log"
                $logFolder = "c:\temp\"
 
                #Create subdirectory if not present
                if (!(Test-Path $logFolder)) {
                    New-Item $logFolder -ItemType Directory > $null
                }

                $logFilePath = Join-Path $logFolder $logFile
 
                $command = 'cmd.exe /C "C:\Program Files (x86)\FireEye\xagt\xagt.exe" -g '+$logFilePath
 
                $expressionresult = Invoke-Expression -Command $command
                $logFileContent = Get-Content $logFilePath -ErrorAction Stop
                $lastTwoLinesOfLogFile = $logFileContent | select -Last 2
                Write-Host $hostname ": The XAGT log file was Successfully created $logFile. Last two lines of the file:" -ForegroundColor Green
                $lastTwoLinesOfLogFile
 
            }
            catch{
                Write-Host $hostname ": Unable to create the XAGT log file." $error[0] -foregroundcolor red
            }
        }


        function Get-DGMTraceResults{
            <#  
            .SYNOPSIS  
                Creates xagt log file  
            .NOTES 
                Author: David Maiolo - david.maiolo@gmail.com  
            .LINK  
            #>
            [CmdletBinding()]
            [OutputType([int])]
            Param
            (
            [string]$traceFile,
            [int]$traceTimeSeconds
            )
 
            $hostname = $env:COMPUTERNAME
            try{
 
                $traceFile = "$hostname`_trace_$(get-date -f yyyy-MM-ddTHHmmss).etl"
                $traceFolder = "c:\temp\"
 
                #Create subdirectory if not present
                if (!(Test-Path $traceFolder)) {
                    New-Item $traceFolder -ItemType Directory > $null
                }

                $traceFilePath = Join-Path $traceFolder $traceFile

                $traceTimeSeconds = 8
            
                #Start the Trace
                Write-Host $hostname ": Starting the trace process..."
                netsh trace start capture=yes tracefile=$traceFilePath

                #Restarting the xagt Service
                Start-DGMService -service xagt

                Write-Host $hostname ": Running trace for $traceTimeSeconds seconds..."
                sleep -Seconds $traceTimeSeconds

                #Stop The Trace
                netsh trace stop

                Write-Host $hostname ": The trace file was run for $traceTimeSeconds seconds and was Successfully created as $traceFile." -ForegroundColor Greenc
 
            }
            catch{
                Write-Host $hostname ": Unable to create the trace file of $traceTimeSeconds seconds." $error[0] -foregroundcolor red
            }
        }
    
 
        #Test FireEye Components Invocation
        Get-DGMTCPPortOpenResults -tcpports 80,443 -destinationAddress "dgmtest-hxdmz-agent-1.helix.apps.fireeye.com"
        Start-DGMService -service xagt
        Write-Host $hostname ": Waiting 8 seconds to see if service is still running..."
        sleep -Seconds 8
        Test-DGMService -service xagt
        Get-DGMTraceResults
        Get-DGMProxyPACFile
        Get-DGMFireEyeLogFile

    }#End Script Block
 
    #Get-Credentials to run script block as
    #$cred = Get-Credential -Message "Enter Credentials With Permissions to Start/Stop xagt Service and Test Ports"
 
    #Invoke the script remotely on each of the computers
    foreach ($computer in $HXEndpointComputers){
        Write-Host "Processing $computer..." -ForegroundColor Cyan
        if (Test-Connection $computer -Count 1 -ErrorAction SilentlyContinue){
            #Invoke the script block on the remote computer
            Invoke-Command -ComputerName $computer -ScriptBlock $script

            #Copy generated log files and scripts
            try{
                Write-Host $computer ": Copying generated logs and traces to $LocalPath..."
                $LocalPathSpecific = Join-Path $LocalPath $computer

                #Create subdirectory if not present
                if (!(Test-Path $LocalPathSpecific)) {
                    New-Item $LocalPathSpecific -ItemType Directory > $null
                }

                $RemotePathUNC = $RemotePath -replace "c:", "\\$computer\c$"
                $LocalPathSpecific = $LocalPathSpecific+"\"

                Copy-Item -LiteralPath $RemotePathUNC -Destination $LocalPathSpecific -Recurse -Force

            }catch{
                Write-Host $computer ": Could not copy the log files."$error[0] -ForegroundColor Red
            }

            
            #Convert .etl file to .cap
            try{
                Write-Host $computer ": converting .etl trace files to .cap files in $LocalPathSpecific...."
                
                $Files =  Get-ChildItem -Recurse $LocalPathSpecific | ? {$_ -like "*.etl"}
 
                $Count = 0
                foreach ( $File in $Files ) {
 
                    $Count++
                    Write-Host $computer ':(' $Count 'of' $Files.Count ') Generating Wireshark file for' $File
 
                    $CAPFile = $File -replace ".etl",".cap"

                    $s = New-PefTraceSession -Path $CAPFile -SaveOnStop
                    $s | Add-PefMessageProvider -Provider $File
                    $s | Start-PefTraceSession
                }
                Write-Host $computer ": Successfully converted the .etl trace files to .cap files." -ForegroundColor Green
            }catch{
                Write-Host $computer ": Could not convert the .etl trace files to .cap files. You may need to install Microsoft Message Analyzer on the computer you are running this script from (https://www.microsoft.com/en-us/download/details.aspx?id=44226)."$error[0] -ForegroundColor Red
            }

            #Cleanup Files on Remote Computer
            try{
                Remove-Item -Path $RemotePathUNC -Recurse
                Write-Host $computer ": Successfully cleaned up the files on $RemotePathUNC." -ForegroundColor Green
            }catch{
                Write-Host $computer ": Could not cleanup the files on $RemotePathUNC."$error[0] -ForegroundColor Red
            }


        }else{
            Write-Host $computer ": The computer is offline." -foregroundcolor yellow
        }
    }
    Write-Host "Run-DGMFireEyeHXCompliance Complete. Please use Microsoft Message Analyzer to view the capture files https://www.microsoft.com/en-us/download/details.aspx?id=44226" -ForegroundColor Magenta

}

function Invoke-DGMFireEyeHXCompliance{
    $HXEndpointComputers = @("DGM-SITE01-TST","DGM-SITE02-TST","VDGMDFS005VER","VDGMDFS006VER","WORKSTATION001","WORKSTATION002","WORKSTATION003") 
    $RemotePath = "c:\temp\"
    $LocalPath = "c:\Run-DGMFireEyeHXCompliance\"

    Run-DGMFireEyeHXCompliance -computers $HXEndpointComputers -RemotePath $RemotePath -localPath $LocalPath
}

Overview

This article is meant to provide you an overview of common troubleshooting tasks centered around client health and upcoming SCCM components. I created several custom tools and scripts to aid in the detection and remediation of several SCCM related troubleshooting tasks. Snippets of those scripts are provided throughout. A Microsoft PFE assisted me in understanding this information and it was used to help a large company improve client health and software update compliance.

 

Log Files

Log File Locations

  • Client

    • %WINDIR%\System32\CCM\Logs
    • %WINDIR%\SysWOW64\CCM\Logs
    • %WINDIR%\CCM\Logs
  • Server

    • <INSTALL_PATH>\Logs

Looking up Log File Errors in CMTrace

CMTrace has the option to look up an error code. For example, you might come across the message: Unable to find or read WUA Managed Server Policy 0x80004005. More information can be found in Tools -> Error Lookup.

Also, searching on the internet for a 0x80004005 error shows us you can rename registry.pol and run gpupdate /force as a potential solution.

Content Distribution

When you distribute content to one your distribution points, the Distribution Manager creates a content transfer job. Next, it notifies the Package Transfer Manager on the site server to transfer the content to the remote distribution points.

Package Transfer Manager logs this process in the pkgxfermgr.log file on the site server.

Content Distribution Troubleshooting (Using Log Files)

  1. smsdbmon.log (SITE SERVER)
    1. SMS_DATABASE_NOTIFICATION component updates the database
    2. Records database changes.
  2. distmgr.log (SITE SERVER)
    1. SMS_DISTRIBUTION_MANAGER component starts the process of adding package to DP
    2. Records details about package creation, compression, delta replication, and information updates.
  3. Monitoring Workspace (SCCM CONSOLE)
  4. PkgXferMgr.log (SITE SERVER)
    1. SMS_PACKAGE_TRANSFER_MANAGER Package Transfer Manager Log
    2. Records the actions of the SMS Executive component that is responsible for sending content from a primary site to a remote distribution point.
  5. smsdbprov.log (remote DP)
    1. Provides information about the SMS Provider

Content Distribution Troubleshooting (By Tracking Content)

Troubleshooting content distribution is important. The following SCCMContentLib folders are located in the Content Library folder on any given distribution point:

  1. P – PkgLib
  2. D – DataLib
  3. F – PkgLib

Tracing content through a distribution point starts by identifying its Package ID in the SCCM console at Monitoring > Overview > Distribution Status > Content Status

Then, using this Package ID, you can further discover the GUID within the associated INI file within PkgLib:

Now using the GUID, you can find the content information in DataLib:

Finally, using the last 4 characters of the hash value within this INI, you can find the differential content in the FileLib directory:

SCCM only stores differential content in order to save space. In other words, these files are the differences between this and a different content folder somewhere else in SCCM of similar files. This is used in order to save space on the distribution point.

The only two places the full content exists is at the source location and final cache on the client such as:

  • \\sccmserver\d$\SOURCE_FILES
  • c:\windows\ccmcache

Distribution points, as you saw in the P. D. F. example above only contain the differential content.

Application Deployment

Enable Enhanced Logging for Application Deployment

Enable enhanced logging to get more intricate details of your application deployment issues.

  1. Enable verbose logging on client: HKLM\Software\Microsoft\CCM\Logging\@GLOBAL

    1. LogLevel = 0
  2. Restart SMS Agent Host service
  3. Enable debug logging on client: HKLM\Software\Microsoft\CC\Logging, Create DebugLogging

    1. Create Enabled = True
  4. Restart SMS Agent Host Service

C:\E8296865\595C7EFE-D7D3-459D-BBE8-E1DF3EF2A18B_files\image001.png

Machine generated alternative text:
Logging 
v @GIobaI 
DebugLogging 
ADALOperationPro 
AlternateHandIer 
AppDiscovery 
AppEnforce 
ApplntentEvaI 
Name 
(Default) 
ab LogDirectory 
LogEnabIed 
LogLeveI 
LogMaxHistory 
LogMaxSize 
Type 
REG SZ 
REG SZ 
REG DWORD 
REG DWORD 
REG DWORD 
REG DWORD 
(value not set) 
CAWINDOWSXCCMXLogs 
DxDDDDDD01 (1) 
DxDDDDDDDO (0) 
DxDDDDDD01 (1) 
DxDD03dD90 (2SDDDO)

 

Troubleshooting Empty CCMCache Folders

In some cases the cache subfolder within C:\Windows\ccmcache for the content will be empty.

  1. Open CMTrace on the client you wish to troubleshoot and browse to C:\Windows\CCM\Logs
  2. Merge the three files below. File -> Open -> Merge Selected Files

    1. L – LocationService.log
    2. C – ContentTransferManager.log
    3. D – DataTransferService.log
  3. Find call backs. Find -> Find What: “calling back”

Calling back with an “empty distribution point list” means there are no DPs for the boundary group (likely no boundary group). Also look at the “Locality=” below the calling back line. This will tell you where the boundary group is. Also look at ContentTransferManager component. This will tell you what DP was actually used.

Troubleshooting Application Deployments

Get Deployment ID of The Application You’re Trying to Troubleshoot

  1. Go to Applications and add column ‘CI Unique ID’ (CONSOLE)
  2. Write down the ‘CI Unique ID’ of problem application.
  3. Write down the ‘Deployment ID’ of problem deployment by either

    1. SQL Studio: Select * from dbo.v_CIAssignment where AssignmentName like ‘%Acrobat%’ <– Assignment_UniqueID
    2. Console: Application -> Deployments -> Show Deployed ID Column

Use That Deployment ID To Track Down Issue

  1. PolicyAgent.log <– Initializing download of policy <Deployment ID> (CLIENT)
  2. Find ‘DTS Job ID’ from log and write it down
  3. DataTransferService.log <- DTSJob <DTS Job ID> created to download… (CLIENT)
  4. PolicyAgent.log <– Applying Policy <Deployment ID> (CLIENT)

Software Updates and Troubleshooting

The software update deployment phase is the process of deploying Microsoft software updates to your workstations and servers. Within SCCM, the updates are typically added to a software update group, the software updates are downloaded to distribution points, and the update group is deployed to clients.

When you deploy software updates, you add the updates to a SUG and then deploy the SUG to your clients. When you create the deployment, the update policy is sent to client computers, and the update content files are downloaded from a DP to the local cache on the client computer. The updates are then available for installation. After the deployment and the deployment policy have been created on the server, clients receive the policy on the next policy evaluation cycle.

Software Update Log Files

Log name

Description

Computer with log file

objreplmgr.log

Records details about the replication of software updates notification files from a parent to child sites.

Site server

PatchDownloader.log

Records details about the process of downloading software updates from the update source to the download destination on the site server.

The computer hosting the Configuration Manager console from which downloads are initiated

ruleengine.log

Records details about automatic deployment rules for the identification, content download, and software update group and deployment creation.

Site server

SUPSetup.log

Records details about the software update point installation. When the software update point installation completes, Installation was successful is written to this log file.

Site system server

WCM.log

Records details about the software update point configuration and connections to the Windows Server Update Services (WSUS) server for subscribed update categories, classifications, and languages.

Site server that connects to the WSUS server

WSUSCtrl.log

Records details about the configuration, database connectivity and health of the WSUS server for the site.

Site system server

wsyncmgr.log

Records details about the software updates synchronization process.

Site system server

WindowsUpdate.log

Records details about when the Windows Update Agent connects to the WSUS server and retrieves the software updates for compliance assessment and whether there are updates to the agent components.

Client

Getting Your Console Ready for Software Update Troubleshooting

Before you can track a deployment, you must first find the Deployment Unique ID of the deployment by adding the Deployment Unique ID column in the console.

Making the Console More Useful

Within the SCCM console, adding the following columns under Software Updates can help when diagnosing issue in the log files and on the client side:

  • Bulletin
  • Article ID
  • Unique Update ID

Troubleshooting Software Updates Using Reports

Windows Update Related Reports

The following reports are available from your SCCM reports server and will allow you to further diagnose server related update issues:

Compliance Reports

Report Name

Description

Compliance 1 – Overall Compliance

Displays the overall compliance data for a software update group.

Compliance 7 – Computers in a specific compliance state for an update group

Displays all computers in a collection that have a specified overall compliance state against a software update group.

Deployment Management Reports

Report Name

Description

Management 2 – Updates required but not deployed

This report returns all software updates that have been detected as required on clients but that have not been deployed to a specific collection. To limit the amount of information returned, you can specify the software update class. Please see my article
Automating Required But Not Deployed Updates to automate this task.

Management 7 – Updates in a deployment missing content

This report returns the software updates in a specified deployment that do not have all of the associated content retrieved, preventing clients from installing the update and achieving 100% compliance for the deployment.

Deployment State Reports

Report Name

Description

States 1 – Enforcement states for a deployment

This report returns the enforcement states for a specific software update deployment, which is typically the second phase of a deployment assessment. For the overall progress of software update installation, use this report in conjunction with “States 2 – Evaluation states for a deployment”.

States 2 – Evaluation states for a deployment

This report returns the evaluation state for a specific software update deployment, which is typically the first phase of a deployment assessment. For the overall progress of software update installation, use this report in conjunction with “States 1 – Enforcement states for a deployment”.

States 5 – States for an update in a deployment (secondary)

This report returns a summary of states for a specific software update targeted by a specific deployment. For best results, start with ‘Management 3 – Updates in a deployment’ to return the software updates contained in a specific deployment, and then drill into this report to return the state for the selected software update.

Scan Report

Report Name

Description

Scan 1 – Last scan states by collection

This report returns the count of computers for a specific collection in each compliance scan state returned by clients during the last compliance scan.

Troubleshooting Report

Report Name

Description

Troubleshooting 2 – Deployment errors

This report returns the deployment errors at the site and a count of computers that are experiencing each error.

How To Track a Windows Update Error (Using Log Files)

If you receive errors in your console in the status area, there are several areas we can look at.

Once you have found an error in the SCCM console, we can continue to troubleshoot by looking at the log files on the affected client.

  1. Open CMTrace on the client you wish to troubleshoot and browse to C:\Windows\CCM\Logs
  2. Merge the three files below. File -> Open -> Merge Selected Files

    1. W – WUAHandler.log
    2. U – UpdateStore.log
    3. S – ScanAgent.log
  3. Filter for missing updates. Tools -> Filter -> Filter when the entry test contains “missing”

If the log file displays “missing” but Add\Remove Programs (appwiz.cpl) shows that the update is installed, you can assume scanning is not working properly.

Further, when looking through the WUHandler column in CMTrace, you can track a deployment as follows:


Windows Update Shows “In Progress”, But You Know Its Installed: State Message Communication

If the UpdateStore.log on the client shows that a particular windows update component is installed, but it is still in progress in the SCCM console, the State Message is likely not communicating properly to the SQL Server.

State messaging is a mechanism in SCCM which replicates point in time conditions on the client.

Fix-DGMSCCMStateMessage Tool

I created the tool Fix-DGMSCCMStateMessage.ps1 which will automatically update the State Message locally on the SCCM client by invoking the following two commands:


$SCCMUpdatesStore = New-Object -ComObject Microsoft.CCM.UpdatesStore
$SCCMUpdatesStore.RefreshServerComplianceState()

The tool requires the –csvfile argument, which is the path to a csv file containing one column, Hostname, with the hostnames listed in the column.

For more information on the latest changes and updates to this tool please see SCCM Script: Fix State Messages

Fixing Windows UpdateStore Corruption (Datastore.edb)

The Windows UpdateStore Datastore.edb in Windows\Software Distribution\.. contains scan results. This may become corrupted.

Fix-DGMSCCMUpdateStore Tool

I created the tool Fix-DGMSCCMUpdateStore.ps1 which will automatically attempt to fix the Windows Update Store on an array of clients imported via a CSV. It accomplishes this by performing the following steps:

  • Stop the Windows Update Service
  • Move SoftwareDistribution to a backup location
  • Start Windows Update Service
  • Recreate SoftwareDistribution

For the latest updates on this tool, please see SCCM Script: Fix Software Update Store

Software Update Points: Troubleshooting Creation

Creating a Software Update Point

  1. Install WSUS 3.0 SP2 (WSUS SERVER)
  2. Administration -> Site Config -> Servers and System Roles -> Create Site -> Update Point (CONSOLE)

Troubleshooting a Software Update Point Creation

  1. sitecomp.log (SITE SERVER)
    1. “SMS_WSUS_CONTROL_MANAGER has been flagged for installation”
    2. Records details about the maintenance of the installed site components on all site system servers in the site.
  2. SUPsetup.log (WSUS SERVER)
    1. Records details about the software update point installation. When the software update point installation completes, Installation was successful is written to this log file.

Errors?

  • The port settings configured for the active SUP must be the same as the port settings configured for the WSUS website in Internet Information Services (IIS) (that is, port 8530).
  • The computer and local Administrator accounts must be able to access virtual directories under the WSUS website in IIS from the site server.

Software Update Points: Troubleshooting Configuration

You have three options when configuring a software update point:

  • Sync from Microsoft Update
  • Sync Upstream data source location (URL)
  • Don’t do either

Troubleshooting Configuration of Software Update Point

  1. WsyncMgr.log (SITE SERVER)
    1. any issues with synchronizing “WSUS server not configured”
    2. Records details about the software updates synchronization process.
  2. WCM.log (SITE SERVER)
    1. Issues with ports or connectivity “Attempting connection to WSUS Server: <SiteServerName, port: <portnumber>, useSSL:<True or False>.” (SERVER)
    2. Records details about the software update point configuration and connections to the Windows Server Update Services (WSUS) server for subscribed update categories, classifications, and languages.
  3. WSUSCtrl.log (SITE SERVER)
    1. configuration or database connectivity issues (SERVER)
    2. Records details about the configuration, database connectivity, and health of the WSUS server for the site.

A Software Update Point Common Problem

  • Both IIS Configuration for WSUS and SUP properties are both using port 8530. However, IIS Config for WSUS is really using port 80.
  • Best thing to do is just wait for the next retry to see if it pulls in the new port.

Software Update Points: Other Issues

Software Update Point Switching/Failover

  • Multiple SUPs? List is given to clients. They choose one at random.
  • Fails? Tries 4 times, 30 mins a part. After 4th attempt, waits 2 minutes and tries next SUP on list.
  • Only certain error codes will trigger SUP to failover.

Verifying Software Update Point in Client Registry

On the client, the Software Update Point can be verified in the following registry location:

HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\WindowsUpdate

Pull Distribution Points

Create a Pull Distribution Point

  1. Administration -> Site Config -> Servers and System Roles -> Create Site -> DP (CONSOLE)
  2. Configure Pull and Source for Pull (CONSOLE)

Troubleshooting Pull Distribution Point

  1. hman.log (SITE SERVER)
    1. “Inserted DP”
    2. Records information about site configuration changes, and the publishing of site information in Active Directory Domain Services.
  2. distmgr.log (SITE SERVER)
    1. “Windows Installer installed the product. ConfigMgr Distribution Point”
    2. Records details about package creation, compression, delta replication, and information updates.
  3. Verify “c:\SMS_DP$” is created (PULL DISTRIBUTION POINT)
  4. pulldp_install.log (PULL DISTRIBUTION POINT)
    1. c:\SMS_DP$\SMS\BIN\ “Windows Installer installed the product. ConfigMgr Distribution Point”
    2. How to confirm whether the Package or application is replicated to PULL DP

What To Do If DISTMGR.LOG Has WMI Error

  1. Check Windows Firewall on the pull distribution point server to see if the connection to the remote WMI provider is being blocked.
  2. Check to see if an anti-virus program might be blocking the communication.
  3. Verify that the site server’s computer account (for example, PrimaryServer$ if PrimaryServer is the name of the server) is part of the local Administrator group on the pull distribution point server.

 

Rotating Management Point

Think of this scenario:

  • 2 Management Points. One with HTTPS with PKI and client has PKI, this one chosen first.
  • If both HTTP, clients prefer MP from their forest.

Scenario: Assume Forests A, B and C. Clients are in C. B has open firewall, A does not. If both B and A are HTTP, who knows if Clients will try to connect to B or A, when they should really only be connecting to A.

Solution: Install MP in C or HTTPS PKI n B.

Client Troubleshooting

Troubleshooting Configuration Manager Client Issues is an important step to understanding why you have certain deployment issues and understanding the overall client health in SCCM.

Understanding Client Health

A client heath task on each client will perform checks to make sure that key areas such as prerequisites, dependent services and WMI are all functioning, and if needed remediate those issues. The Configuration Manager Health Evaluation runs as a schedule task and launches an executable called CCMEval.EXE which will perform checks and remediation listed in the CCMEval.XML file. This scheduled task is called “Configuration Manager Health Evaluation” when vieyoud in Task Scheduler.

The results of the CCMEval task can be vieyoud in the Monitoring > Client Status > Client Check area of the console:

Client Health Reports

Your reports server has several client health reports available. Searching for “Client Health” yields plenty of useful results, including a new Dashboard – Client Health Statistics report that can imported through a special Microsoft PFE engagement (details on this below).

Baseline Collections

In order to create a good baseline for your other collections to limit against, the following best practice queries are recommended to create the baseline collections:

Exclude Inactive Clients


select SMS_R_SYSTEM.ResyourceID,SMS_R_SYSTEM.ResyourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResyourceDomainORWorkgroup,SMS_R_SYSTEM.Client 
from SMS_R_System inner join SMS_G_System_CH_ClientSummary on SMS_G_System_CH_ClientSummary.ResyourceId = SMS_R_System.ResyourceId 
where SMS_G_System_CH_ClientSummary.ClientActiveStatus = 0

Exclude Heartbeat Discovery That is Greater Date > 14 Days

select SMS_R_SYSTEM.ResyourceID,SMS_R_SYSTEM.ResyourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResyourceDomainORWorkgroup,SMS_R_SYSTEM.Client 
from SMS_R_System 
where SMS_R_System.ResyourceId in (select ResyourceID from SMS_R_System where (SMS_R_SYSTEM.AgentTime <= DateAdd(dd,-14,getdate())) and AgentName = 'Heartbeat Discovery') and SMS_R_System.ResyourceId NOT in (select ResyourceID from SMS_R_System where (SMS_R_SYSTEM.AgentTime > DateAdd(dd,-14,getdate())) and AgentName = 'Heartbeat Discovery') and SMS_R_System.ResyourceId in (select ResyourceId from SMS_G_System_CH_ClientSummary Where ClientActiveStatus = 1)

Exclude HW Missing

select SMS_R_SYSTEM.ResyourceID,SMS_R_SYSTEM.ResyourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResyourceDomainORWorkgroup,SMS_R_SYSTEM.Client 
from SMS_R_System inner join SMS_G_System_COMPUTER_SYSTEM on SMS_G_System_COMPUTER_SYSTEM.ResyourceID = SMS_R_System.ResyourceId 
where SMS_G_System_COMPUTER_SYSTEM.Model is null and SMS_R_System.ResyourceId in (select ResyourceId from SMS_G_System_CH_ClientSummary Where ClientActiveStatus = 1)

Exclude HW Inventory Greater Than 30 Days

select SMS_R_SYSTEM.ResyourceID,SMS_R_SYSTEM.ResyourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResyourceDomainORWorkgroup,SMS_R_SYSTEM.Client 
from SMS_R_System where SMS_R_System.ResyourceId not in (select SMS_R_System.ResyourceId from SMS_R_System inner join SMS_G_System_WORKSTATION_STATUS on SMS_G_System_WORKSTATION_STATUS.ResyourceId = SMS_R_System.ResyourceId 
where SMS_G_System_WORKSTATION_STATUS.LastHardwareScan >= DateAdd(dd, -30, getdate())) and SMS_R_System.ResyourceId in (select ResyourceId from SMS_G_System_CH_ClientSummary Where ClientActiveStatus = 1)

Exclude SW Inventory Greater Than 30 Days

select SMS_R_SYSTEM.ResyourceID,SMS_R_SYSTEM.ResyourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResyourceDomainORWorkgroup,SMS_R_SYSTEM.Client 
from SMS_R_System 
where SMS_R_System.ResyourceId not in (select ResyourceID from SMS_R_System inner join SMS_G_System_LastSoftwareScan on SMS_G_System_LastSoftwareScan.ResyourceID = SMS_R_System.ResyourceId where SMS_G_System_LastSoftwareScan.LastScanDate >= DateAdd(dd,-30,getdate())) and SMS_R_System.ResyourceId in (select ResyourceId from SMS_G_System_CH_ClientSummary Where ClientActiveStatus = 1)

These queries can be used to create new baseline collections and exclusions for those collections

Advanced Troubleshooting: SCCM ToolKit

Use the SCCM toolkit for advanced troubleshooting tools. It contains fifteen downloadable tools to help you manage Configuration Manager.

Installer: ConfigMgrTools.msi (download the latest via Bing)

Client Based Tools

  • Client Spy – A tool that helps you troubleshoot issues related to software distribution, inventory, and software metering on System Center 2012 Configuration Manager clients.
  • Configuration Manager Trace Log Viewer – A tool used to view log files created by Configuration Manager components and agents.
  • Deployment Monitoring Tool – The Deployment Monitoring Tool is a graphical user interface designed help troubleshoot Applications, Updates, and Baseline deployments on System Center 2012 Configuration Manager clients.
  • Policy Spy – A policy viewer that helps you review and troubleshoot the policy system on System Center 2012 Configuration Manager clients.
  • Power Viewer Tool – A tool to view the status of power management feature on System Center 2012 Configuration Manager clients.
  • Send Schedule Tool – A tool used to trigger a schedule on a client or trigger the evaluation of a specified DCM Baseline. You can trigger a schedule either locally or remotely.
  • Wakeup Spy – A tool that provides a view of the power state of Configuration Manager client computers and which operate as managers or manages.

Machine generated alternative text:
CliSpy.exe 
CMTrace.exe 
DeploymentMonitoringTooI.exe 
PolicySpy.exe 
power"vr.exe 
SendScheduIe.exe 
SendScheduIeMessages.xmI 
SleepAgentWS.dII 
WakeupSpy.exe

Server Based Tools

  • DP Job Manager – A tool that helps troubleshoot and manage ongoing content distribution jobs to Configuration Manager distribution points.
  • Collection Evaluation Viewer – A tool that assists in troubleshooting collection evaluation related issues by viewing collection evaluation details.
  • Content Library Explorer – A tool that assists in troubleshooting issues with and viewing the contents of the content library.
  • Security Configuration Wizard Template for Microsoft System Center 2012 R2 Configuration Manager – The Security Configuration Wizard (SCW) is an attack-surface reduction tool for the Microsoft Windows Server 2008 R2 operating system. Security Configuration Wizard determines the minimum functionality required for a server’s role or roles, and disables functionality that is not required.
  • Content Library Transfer – A tool that transfers content from one disk drive to another.
  • Content Ownership Tool – A tool that changes ownership of orphaned packages (packages without an owner site server).
  • Role-based Administration Modeling and Auditing Tool – This tool helps administrators to model and audit RBA configurations.
  • Run Metering Summarization Tool – The purpose of this tool is to run the metering summarization task to analyze raw metering data

Machine generated alternative text:
AdminU WqIQueryEngine.dII 
CEViewer.exe 
ConfigMgr2012SCWxml 
ConfigMgrSCWHeIper .dll 
ContentLibraryExpIorer.exe 
ContentLibraryTransfer.exe 
ContentOwnershipTooI.exe 
DPJobMgr.exe 
Microsoft.ConfigurationManagement.M... 
RBAViewer.exe 
runmetersumm.exe

Infrastructure Health (SQL and Site Server)

The SCCM infrastructure health is vital to the overall system responsiveness and functionality for your installation.

Bandwidth Throttling

Within the SCCM console, bandwidth throttling is available to the Distribution Points at Administration > Overview > Distribution Points -> Properties -> Rate Limits.

Limited to a specific maximum: This method allows you to limit bandwidth to a configured percentage by hyour as a time slice.

Pulse mode throttling is also available, which divides the data into data blocks, transmitted at a time interval. In your example above, 20KB would transmit every second, or 20KBps.

SQL Memory Allocation

Your SCCM SQL instance is handled by your SQL Server or SQL Failover CNO.

If you have a virtual SQL server, you can use the Virtual Machine Manager Console to first increase the minimum and maximum memory available to the two SQL servers:

Opening SQL Management Studio allows you to then adjust the maximum memory used by the SQL service

Site Server Memory Allocation

Using the Virtual Machine Manager Console you can verify your SCCM site server has adequate memory allocation:

TempDB Memory Allocation

Opening SQL Management Studio now allows you to verify and adjust the initial size and autogrowth settings on the Tempdb database.

Console Latency

Rebuilding Indexes can help maintain the SCCM SQL database efficiency. To turn on database indexing, go to

Administration > Site Configuration > Sites

To help with console latency, the rebuild index task can enabled with a Sunday 12:00AM to 5:00AM schedule.

Data Discovery Record (DDR) Workflow

When SCCM Discovery runs, it creates discovery data records (DDRs). The information contained in a DDR varies depending upon the discovered resyource. For example, it can include the NetBIOS name of a computer, the IP address and IP subnet of a computer or device, and the computer operating system name.

DDRs are sent to the site server inbox located as a .DDR file:

  • \\siteserver\C$\Program Files\Microsoft System Center Configuration Manager\inboxes\auth\ddm.box

Once processed, the .DDR file is erased. If many .DDR records are visible, that likely means Active Directory discovery in SCCM is set to too short of an interval.

Additional values in DDR files appear within the SCCM console in the client properties of an asset. Although it is not officially supported, one can create their own DDRs to be processed by the site server by

  1. Creating a new instance of the SMSResGen class.
  2. Creating a new DDR by using the NewDDR method.
  3. Adding properties to the DDR by using the ADDPROP_ methods.
  4. Writing the new DDR to a file by using the DDRWrite method.

The site server can process multiple DDRs for the same asset, as is the case in a custom PFE engagement that was mentioned during your engagement.

What’s New in SCCM

This guide is meant to summarize the latest features you can expect in the SCCM 1702, 1706 and 1710. Most of the information from these features was obtained by summarizing the information contained in Microsoft’s SCCM documentation. (What’s new in version 1710 of System Center Configuration Manager, What’s new in version 1706 of System Center Configuration Manager and What’s new in version 1702 of System Center Configuration Manager)

New Features in SCCM 1702


Operating System Deployment (OSD)

  • Expire stand-alone media (expire date)
  • Package ID displayed in Task Sequence Step
  • Try again when a task sequence fails
  • BIOS to UEFI (MBR2GPT /convert /disk:0 /AllowFullOS)

MDM (Intune Managed Devices)

  • Compliance settings for iOS to match Intune
  • deploy volume-purchased iOS apps
  • Don’t need to specify mobile OS version when creating new policies and profiles for Intune-managed devices
  • Android for Work support
  • New device compliance policy rule is available to help you block access to corporate resources that support conditional access.
  • Deploy Office 365 apps to clients: Use O365 Client Management dashboard

Software Updates

  • “Available for Install” and “Ready for Download” states added.
  • If more than one update applies to environment, only one is downloaded.
  • ‘EasySetupPayload’ folder on your site server is cleaned up automatically.
  • Software Update Point: Use boundary groups to find a new software update point, and can now fallback if theirs isn’t found.

Protect devices

  • Detect outdated antimalware client versions
  • Device health attestation service can now be managed
  • Windows 10 notification informs end users that they must take additional actions to complete Windows Hello for Business setup.

In Console Search

  • Object Path property added
  • search text is saved as you click different nodes

Misc.

  • Feedback button available (send feedback to Microsoft)
  • Data Warehouse service point added. Holds historical SCCM data.(up to 2TB)
  • Peer Cache: Won’t request from peer if low battery, CPU >80%, Disk I/O exceeds 10
  • Content library cleanup tool cleans up unneeded content from DPs
  • Windows Store for Business Apps: Can deploy apps from the Windows Store for Business to Windows 10
  • Software Deployment: Check for running executable files before install.

New Features in SCCM 1706


MDM (Intune Managed Devices)

  • New Configuration items (Windows 10 Devices)

    • Password (Device Encryption)
    • Device (System time modification)
    • Store (Auto-update apps from store)
    • Microsoft Edge (Default search engine)
  • New device compliance policy rules (Android, iOS)

    • Required password type
    • Block USB debugging on device.
    • Block apps from unknown sources
    • Require threat scan on apps
  • New MAM Policy Settings

    • Block screen capture (Android devices only)
  • Enrollment Restrictions

    • Set users cannot enroll personal Android or iOS devices

Operating System Deployment (OSD)

  • Hardware inventory collects Secure Boot information
  • Collapsible-view task sequence groups
  • Reload boot images with current Windows PE version

Software Updates

  • After failing to reach that SUP for 2 hours, the client then checks its pool of available software update points
  • Manage Microsoft Surface driver updates

Azure AD integration

  • Azure Services Wizard – This Wizard provides a common configuration experience that replaces the individual workflows to set up certain Azure services
  • Use Azure AD to authenticate clients on the Internet to access sites. Azure AD replaces the need to configure and use client authentication certificates. This requires the cloud management gateway site system role.
  • Install and manage the client on Internet PCs. This requires the use of the cloud management gateway site system role.
  • Configure Azure AD User Discovery.

Site Infrastructure / Misc

  • Run PowerShell scripts from the Configuration Manager console (released)
  • Client Peer Cache supports express installation files for Win10 and O365
  • Data Warehouse is no longer a pre-release feature
  • In-Console Updates: CMUpdateReset.exe will reset failed updates.

New Features in SCCM 1710


MDM (Intune Managed Devices)

  • Co-management for Windows 10 1709 (Fall Creators Update) devices
    • Use SCCM and Intune. Provides a bridge from traditional to modern management.
  • New MAM Policy Settings

    • Disable printing
    • Disable contact sync
  • Actions for non-compliance

    • Configure a time-ordered sequence of actions that are applied to devices that fall out of compliance. For example, notify users of non-compliant devices via e-mail, then mark non-compliant
  • Windows 10 ARM64 device support

Operating system deployment (OSD)

  • Add child task sequences to a task sequence

Protect devices

  • Create and deploy Exploit Guard policies

    • Only for Windows 10 1709 Fall Creators Update
    • New set of host intrusion prevention capabilities for Windows 10,
  • Create and deploy Windows Defender Application Guard policy

    • Only for Windows 10 1709 Fall Creators Update
    • If an employee goes to an untrusted site through either Microsoft Edge or Internet Explorer, Microsoft Edge opens the site in an isolated Hyper-V-enabled container, which is separate from the host operating system.

Site Infrastructure / Misc

  • Restart computers from the console
  • Run PowerShell scripts from the Configuration Manager console (updated features – was released in 1702)
    • Use Security Scopes to define who can run them
    • Real-time monitoring of the scripts
  • Software Center customization: Add enterprise branding elements, specify the visibility of tabs, add company name, set a color theme, set a company logo, and set the visible tabs for client devices

Overview

I created this application, Threaded Computer Details, to allow you to have a single point of data aggregation for common SCCM, DHCP and Active Directory metrics on the computers in your environment, and wrap those metrics around a convenient program with search options.

You can either search initially by computer name, or if User-Device Affinity is available in your environment, by user.

The application performs several initial network operations in a threaded hierarchy of ICMP -> RDP -> c$ -> SCCM queries -> AD Queries which provides efficiency when aggregating data on all of your machines.

Basic Use of Application

The application can be called directly, which will bring up a user interface for searching:


Get-DGMThreadedComputerDetails -ProviderMachineName SCCMSERVERNAME -Sitecode SITE -domaincontroller DOMAINCONTROLLER01

Or can be given input from the pipeline where it will not prompt for any interaction and would return the results as an array:

$mycomputerlist | Get-DGMThreadedComputerDetails -ProviderMachineName SCCMSERVERNAME -Sitecode SITE -domaincontroller DOMAINCONTROLLER01

It also supports switches for .CSV import and export via –csvimport and –csvexport, where both expect a path to a .CSV file.

As mentioned, the application returns the data as an array, which could easily be viewed in a GUI by piping the results to Out-Gridview as in this example:


$myservers | Get-DGMThreadedComputerDetails -ProviderMachineName SCCMSERVERNAME -Sitecode SITE -domaincontroller DOMAINCONTROLLER01 | Out-Gridview

Application Features

Given just a list of computer names, or a list of company user accounts, this program will attempt to find and return:

  • Name
  • Pingable Status
  • IP Address and IP Scope
  • AD Enabled/Disabled Status
  • RDP Enabled Status
  • Connect to c$ Status
  • VirtualCNO Status for Virtual Server Obects
  • SCCM Client Installation Status
  • Primary User Based On a Hierarchal Algorithm Including SCCM Pending User Device Affinity
  • User Source
  • User’s Company Title
  • User’s Company Department
  • User’s E-Mai
  • Other Users via User Device Affinity
  • Operating System
  • Manufacturer
  • Model
  • Serial Number
  • BIOS Version
  • Architecture (x86 / x64)
  • MAC Addresses
  • AD Description
  • DHCP Scope Location
  • AD Creation Date
  • AD Canonical Name
  • Last Time Online
  • Last Time Offline
  • Predetermined User

Getting Your Environment Ready for Threaded Computer Details 1.0

This application was written in PowerShell and needs to be run on a machine on your corporate network that has access to all the computers in your environment with ability to:

  • Ping (ICMP)
  • Check for RDP (3389)
  • Check for C$ share (if desired).

In addition, it needs access to read the DHCP scopes from your domain controller, and to do so needs the RSAT-DHCP tools installed:

Install-WindowsFeature -Name RSAT-DHCP 

In addition, it and also needs the SCCM Console installed on the computer it is run from, as it utilizes the SCCM modules to gather serial number, model, user-device affinity data, etc. To install the console, you could typically run something such as:

\\sccmserver\c$\Program Files\Microsoft System Center Configuration Manager\tools\ConsoleSetup\console.msi

Advanced Use of Application

If launched directly, the application provides you with several main search options:

[1] Search for single computer name:

Search for results on a single computer, such as MYPERSONALCOMPUTER01

[2] Search by wildcard computer name

Type a partial computer name and all computers matching those results will be returned, as in this example where we search for all computers with VDGMSC in the hostname:

[3] Search by description

Search via active directory description. Results will be returned for any computer containing the AD description query you specify.

[4] Search by Operating System

Partial matches work fine. For example, search for 2012 to return all of your Windows Sever 2012 computers. For this query, the OS is queried from Active Directory, not SCCM.

[5] Search by User Name (DOMAIN\USER)

If user-device affinity is enabled in your environment, you can search for what computers belong to what users.


[6] Import CSV (with PCs as ‘Name’ column)

Have a .CSV ready with a column header Name and underneath it with all of your computer names.

[7] Import CSV (with Users as ‘PredeterminedUser’ column)

Have a .CSV ready with a column header PredeterminedUser and underneath it with all of your user names in the format domain\user.

Use Case 1: Send Out an Automated E-Mail Report of All Important SCCM Non-Client Computers

Utilizing my HTML Email functions, here we import a list of non-compliant computers from SCCM and run them against the script. From there, we filter out computers than are non-enabled in AD, are not-pingable, etc. Then we break the list up by Server and Workstation and email it out.

Use Case Wrapper Function: Run-DGMSCCMNoClientReport.ps1


Import-Module "\\scriptserver\Scripts\Get-DGMThreadedComputerDetails\Get-DGMThreadedComputerDetails.psm1" -Force
Import-Module "\\scriptserver\scripts\New-DGMEmailReport\New-DGMEmailReport.psm1" -Force

function Run-DGMSCCMNoClientReport{
    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$ProviderMachineName,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateLength(3,3)]
        [String]$Sitecode
    )

    #Create Some Arrays Of Data To Display in Report. You can create as many as you want.
    $OutputArrays = @()

    # Import the ConfigurationManager.psd1 module
    $module = "$($ENV:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1"
    if((Get-Module ConfigurationManager) -eq $null) {
        Write-Host Importing $module ...
        Import-Module $module -Force
    }

    # Connect to the site's drive if it is not already present
    if((Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue) -eq $null) {
        $NewPSDrive = New-PSDrive -Name $SiteCode -PSProvider CMSite -Root $ProviderMachineName
    }

    # Set the current location to be the site code.
    Set-Location "$($SiteCode):\"

    $NonClients = Get-CMCollectionMember -CollectionName "All Non-Client Systems" | Select Name | Get-DGMThreadedComputerDetails -ProviderMachineName $ProviderMachineName -Sitecode $Sitecode -domaincontroller $domaincontroller
    $NonClientsWorkstations = $NonClients | Where-Object {$_."AD Enabled" -eq $true -and $_.Pingable -eq $true -and $_.VirtualCNO -ne $true -and $_."Operating System" -Clike "*Windows*" -and $_."Operating System" -cnotlike "*Server*"}
    $NonClientsServers = $NonClients | Where-Object {$_."AD Enabled" -eq $true -and $_.Pingable -eq $true -and $_.VirtualCNO -ne $true -and $_."Operating System" -Clike "*Windows*" -and $_."Operating System" -clike "*Server*"}

    #Array1
    $output = [PSCustomObject] @{
    'Message' = "These are all of the workstations that DO NOT HAVE THE SCCM CLIENT, however ARE PINGABLE and ENABLED IN AD. These workstations are not receiving Windows Updates or Software deployments from SCCM. 
    These workstations need to be investigated and potentially have the SCCM client installed manually. Source Location: \\scriptserver\c$\Program Files\Microsoft System Center Configuration  Manager\Client Installation Command: CCMSetup.exe /mp:SCCMSITESERVER /logon SMSSITECODE=AUTO";
    'Title' = "Non-Client Workstations";
    'Color' = "Red";
    'Array' = $NonClientsWorkstations | Select "Name","IP Address","RDP Enabled","Connect to c$","Operating System","Primary User","AD Description","DHCP Scope Location","AD Creation Date","Canonical Name" | Sort-Object -Property Name
    }
    if ($output.Array -ne $NULL){$OutputArrays+=$output}

    #Array2
    $output = [PSCustomObject] @{
    'Message' = "These are all of the SERVERS that DO NOT HAVE THE SCCM CLIENT, however ARE PINGABLE, ENABLED IN AD, and ARE NOT A VIRTUAL CNO. Please verify these servers are in the DMZ, etc..";
    'Title' = "Non-Client Servers";
    'Color' = "Red";
    'Array' = $NonClientsServers | Select "Name","IP Address","RDP Enabled","Connect to c$","Operating System","Primary User","AD Description","DHCP Scope Location","AD Creation Date","Canonical Name" | Sort-Object -Property Name
    }
    if ($output.Array -ne $NULL){$OutputArrays+=$output}

    #Multiple Arrays
    New-DGMEmailReport `
        -Arrays $OutputArrays `
        -ReportTitle "SCCM Non-Client Computers Report" `
        -from "SCCMDeployments@company.com" `
        -To "dmaiolo@company.com" `
        -subject "Report: SCCM Non-Clients That Are of Concern" `
        -AttatchResults 
}

Run-DGMSCCMNoClientReport -ProviderMachineName SCCMSITESERVER -Sitecode DGM

Use Case 2: Create a Comprehensive Computer Details Report of All Computers in Environment

Piping the SCCM “All Systems” collection as the input for the application, we can build a comprehensive details list of all equipment in the environment.

$ComprehensiveReport = Get-CMCollectionMember -CollectionName "All Systems" | Select Name | Get-DGMThreadedComputerDetails -ProviderMachineName $ProviderMachineName -Sitecode $Sitecode -domaincontroller $domaincontroller -csvoutput "C:\comprehensive_report.csv"

We could also run the data through an Excel PivotChart for some interesting metrics:

Use Case Wrapper Function: Run-DGMComprehensiveDetailsReport.ps1


Import-Module "\\scriptserver\Scripts\Get-DGMThreadedComputerDetails\Get-DGMThreadedComputerDetails.psm1" -Force
Import-Module "\\scriptserver\scripts\New-DGMEmailReport\New-DGMEmailReport.psm1" -Force

function Run-DGMComprehensiveDetailsReport{
    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$ProviderMachineName,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateLength(3,3)]
        [String]$Sitecode
    )

    #Create Some Arrays Of Data To Display in Report. You can create as many as you want.
    $OutputArrays = @()

    # Import the ConfigurationManager.psd1 module
    $module = "$($ENV:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1"
    if((Get-Module ConfigurationManager) -eq $null) {
        Write-Host Importing $module ...
        Import-Module $module -Force
    }

    # Connect to the site's drive if it is not already present
    if((Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue) -eq $null) {
        $NewPSDrive = New-PSDrive -Name $SiteCode -PSProvider CMSite -Root $ProviderMachineName
    }

    # Set the current location to be the site code.
    Set-Location "$($SiteCode):\"

    $ComprehensiveReport = Get-CMCollectionMember -CollectionName "All Systems" | Select Name | Get-DGMThreadedComputerDetails -ProviderMachineName $ProviderMachineName -Sitecode $Sitecode -domaincontroller $domancontroller -csvoutput "C:\comprehensive_report.csv"
}

Run-DGMComprehensiveDetailsReport -ProviderMachineName SCCMSERVER001 -Sitecode DGM 

Main Application: Get-DGMThreadedComputerDetails.psm1

This is the main application code.


<#
.Synopsis
   Get Computer Details
.DESCRIPTION
   The tool, Get-DGMThreadedComputerDetails.ps1, will build a detailed list of the computers or users you provide to it. 
.AUTHOR
   David Maiolo
#>

<#
.Synopsis
   Returns a True if name value is a Cluster Name Object/Virtual Computer Object
#>
function Get-DGMClusterComputerObject{
    [CmdletBinding()]
    Param
    (
        # Param1 help description
        [Parameter(
                   Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0)]
        [ValidateScript({(Get-ADComputer -Identity $_).objectclass -eq 'computer' })]
        $Name

    )

    Begin
    {$Computer= Get-ADComputer $name -Properties serviceprincipalname
    }
    Process
    { If( $Computer.servicePrincipalName -contains 'DGMClusterVirtualServer/' + $Computer.Name){$result=$true}
      Else{$Result=$False}
    }
    End
    {$result
    }
}

Function Get-DGMThreadedComputerDetails{
    param(
        [Parameter(Position=0,Mandatory=$false,ValueFromPipeline=$true)]
        $input,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$false)]
        [String]$ProviderMachineName,
        [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$false)]
        [ValidateLength(3,3)]
        [String]$Sitecode,
        [Parameter(Position=3,Mandatory=$true,ValueFromPipeline=$false)]
        [String]$domaincontroller,
        [Parameter(Position=4,Mandatory=$false,ValueFromPipeline=$false)]
        [ValidateScript({(Test-Path $_)})]
        [String] $csvinput,
        [Parameter(Position=5,Mandatory=$false,ValueFromPipeline=$false)]
        [ValidateScript({($_ -le 100 -and $_ -gt 0)})]
        [int] $threads=100,
        [Parameter(Position=6,Mandatory=$false,ValueFromPipeline=$false)]
        [String] $csvoutput,
        [Parameter(Position=7,Mandatory=$false,ValueFromPipeline=$false)]
        [Switch] $excludeCNO
    )

    $path = (get-item -Path .).FullName
    $arraytoping=@()

    #Header
    Write-Host "==========================================" -ForegroundColor Cyan
    Write-Host "Get Threaded Computer Details" -ForegroundColor Cyan
    Write-Host "v1.0 (2018-02-14) by dmaiolo" -ForegroundColor Cyan
    Write-Host "==========================================" -ForegroundColor Cyan

    #Check for what arguments were passed
    if([bool]($MyInvocation.BoundParameters.Keys -match 'csvinput')){
        Write-Host "Importing $csvinput..."
        $csvimport = import-csv $csvinput
        <#
        $csvimport | foreach-object {
            $temparray = [PSCustomObject] @{'Name' = $_.Name}
            $arraytoping += $temparray 
            Write-Host "Importing"($_.Name)"..."
            }#>
        $arraytoping = $csvimport    
    }
    elseif([bool]($MyInvocation.BoundParameters.Keys -match 'input')){
        Write-Host "Importing from pipeline..."
        foreach ($object in $input) {
            $arraytoping += $object
        }
    }else{
        Write-Host "Manual Input Selected."
        $arraytoping = Get-DGMSearchQueryComputers -ProviderMachineName $ProviderMachineName -Sitecode $Sitecode
    }

    
    if ($arraytoping.count -lt 1){
        Write-Host "No Computers Were Found"
        break
    }


    if([bool]($MyInvocation.BoundParameters.Keys -match 'csvoutput')){
        $csvoutputcheck = $true
    }

    #Ping the computers
    $pingablecomputers = Get-DGMOnlineComputers -ComputerList $arraytoping -Threads $threads
    $pingablecomputers | add-member –membertype NoteProperty –name Pingable –Value True -Force

    #======================

    #Check RDP On the Pingable Computers
    $RDPEnableComputers = Get-DGMRDPComputers -ComputerList $pingablecomputers -Threads $threads
    $RDPEnableComputers | add-member –membertype NoteProperty –name RDPEnabled –Value True -Force

    #Create Non-RDP Enabled Computers Array
    $NonRDPEnabledComputers = $pingablecomputers | where {$RDPEnableComputers -notcontains $_}
    $NonRDPEnabledComputers | add-member –membertype NoteProperty –name RDPEnabled –Value False -Force

    #Combining RDP Enabled and Non-Enabled Computers into One Araay
    $RDPDetailedPingableComputers = @()
    $RDPDetailedPingableComputers+=$RDPEnableComputers
    $RDPDetailedPingableComputers+=$NonRDPEnabledComputers

    #======================


    #Check Path On the Pingable Computers
    $PathEnableComputers = Get-DGMTestPath -ComputerList $RDPDetailedPingableComputers -Threads $threads -Path "c$"
    $PathEnableComputers | add-member –membertype NoteProperty –name PathEnabled –Value True -Force

    #Create Non-Path Enabled Computers Array
    $NonPathEnabledComputers = $RDPDetailedPingableComputers | where {$PathEnableComputers -notcontains $_}
    $NonPathEnabledComputers | add-member –membertype NoteProperty –name PathEnabled –Value False -Force

    #Combining Path Enabled and Non-Enabled Computers into One Araay
    $PathRDPDetailedPingableComputers = @()
    $PathRDPDetailedPingableComputers+=$PathEnableComputers
    $PathRDPDetailedPingableComputers+=$NonPathEnabledComputers

    #======================

    #Create Non-Pingable Array
    $nonpingablecomputers = $arraytoping | where {$pingablecomputers -notcontains $_} 
    $nonpingablecomputers | add-member –membertype NoteProperty –name Pingable –Value False -Force
    $nonpingablecomputers | add-member –membertype NoteProperty –name RDPEnabled –Value $null -Force
    $nonpingablecomputers | add-member –membertype NoteProperty –name PathEnabled –Value $null -Force

    #Combine Everything into one giant final array
    $finalresults = @()
    $pingedresults = @()
    $pingedresults+=$PathRDPDetailedPingableComputers
    $pingedresults+=$nonpingablecomputers

   
    #Gather the DHCP Scope
    try{$dhcp_scopes = Get-DhcpServerv4Scope –ComputerName $domaincontroller}catch{Write-Host "No Access to Domain Controller $domaincontroller" -ForegroundColor Red;}

    $pingedresults | foreach-object {
       $Name = $_.Name
       $PredeterminedUser = $_.PredeterminedUser
       $RDPEnabled = $_.RDPEnabled
       $PathEnabled = $_.RDPEnabled
       
       #Note if CNO Object
       try{
        if((Get-DGMClusterComputerObject $Name) -eq $false) {$VirtualCNO = $null}else{$VirtualCNO = $TRUE}

       }catch{
        $VirtualCNO =$null
       }

        #If they were pingable, get the DHCP Scope and IP Address
        if ($_.Pingable -eq "True"){ 
            Write-Host "Processing Pinged $Name..."
            
            try{
                $ComputerObject = Get-ADComputer $Name -Properties Created,Description,OperatingSystem,CanonicalName,Enabled,extensionAttribute1,extensionAttribute2,extensionAttribute4,extensionAttribute5 | Select Name,Created,Description,OperatingSystem,CanonicalName,Enabled,extensionAttribute1,extensionAttribute2,extensionAttribute4,extensionAttribute5

                #Get IP Address
                if ($ComputerObject.IPv4Address){
                    $IPv4Address = ($ComputerObject.IPv4Address).ToString()

                    $IPv4Scope = (([ipaddress] $IPv4Address).GetAddressBytes()[0..2] -join '.').ToString()
                    $IPv4ScopesDescription = ($dhcp_scopes | Select-Object -Property Name,ScopeId | Where-Object ScopeId -Like "$IPv4Scope.*").Name
                }
                #If IP Was not in AD, Ping it and get it (mostly redundant at this point)
                elseif($IPv4Address = (Test-Connection -ComputerName $Name -Count 1 -ErrorAction SilentlyContinue).IPV4Address.IPAddressToString){
                    $IPv4Scope = (([ipaddress] $IPv4Address).GetAddressBytes()[0..2] -join '.').ToString()
                    $IPv4ScopesDescription = ($dhcp_scopes | Select-Object -Property Name,ScopeId | Where-Object ScopeId -Like "$IPv4Scope.*").Name
                }
                # No IP Found? Make these blank (IP should be found though, just a catch all
                else{
                    $IPv4Address = $null
                    $IPv4ScopesDescription = $null
                    $IPv4Scope = $null
                }
            }catch{
                $ComputerObject = $null
                $IPv4Address = $null
                $IPv4Scope = $null
                $IPv4ScopesDescription = $null
            }

        }
        #Since they are not pingable, don't bother with IP Address and Scope
        else{
            Write-Host "Processing Unpingable $Name..." -ForegroundColor Yellow
            try{
                $ComputerObject = Get-ADComputer $Name -Properties Created,Description,OperatingSystem,CanonicalName,Enabled,extensionAttribute1,extensionAttribute2,extensionAttribute4,extensionAttribute5 | Select Name,Created,Description,OperatingSystem,CanonicalName,Enabled,extensionAttribute1,extensionAttribute2,extensionAttribute4,extensionAttribute5
            }catch{
                $ComputerObject = $null
            }
            $IPv4Address = $null
            $IPv4ScopesDescription = $null
            $IPv4Scope = $null
            $RDOpen = $null
        }

        #Get AD Device Details
        if ($ComputerObject.Created){$Created = ($ComputerObject.Created).ToString()}else{$Created = $null}
        if ($ComputerObject.Description){$Description = ($ComputerObject.Description).ToString()}else{$Description = $null}
        if ($ComputerObject.OperatingSystem){$OperatingSystem = ($ComputerObject.OperatingSystem).ToString()}else{$OperatingSystem = $null}
        if ($ComputerObject.CanonicalName){$CanonicalName = ($ComputerObject.CanonicalName).ToString()}else{$CanonicalName = $null}
        if ($ComputerObject.Enabled){$Enabled = ($ComputerObject.Enabled).ToString()}else{$Enabled = $null}
        if ($ComputerObject.extensionAttribute1){$SME1 = ($ComputerObject.extensionAttribute1).ToString()}else{$SME1 = $null}
        if ($ComputerObject.extensionAttribute2){$SME2 = ($ComputerObject.extensionAttribute2).ToString()}else{$SME2 = $null}
        if ($ComputerObject.extensionAttribute4){$MW1 = ($ComputerObject.extensionAttribute4).ToString()}else{$MW1 = $null}
        if ($ComputerObject.extensionAttribute5){$MW2 = ($ComputerObject.extensionAttribute5).ToString()}else{$MW2 = $null}

        #Get SCCM Device Details
        try{
            $DeviceDetails = Get-DGMSCCMDeviceDetails -Hostname $Name -ProviderMachineName $ProviderMachineName -Sitecode $SiteCode

            $IsClient = $DeviceDetails.IsClient
            $CMUserName = $DeviceDetails.UserName
            $LastStatusMessage = $DeviceDetails.LastStatusMessage
            $LastSoftwareScan  = $DeviceDetails.LastSoftwareScan
            $LastPolicyRequest = $DeviceDetails.LastPolicyRequest
            $LastDDR = $DeviceDetails.LastDDR
            $LastClientCheckTime = $DeviceDetails.LastClientCheckTime
            $LastActiveTime  = $DeviceDetails.LastActiveTime
            $IsVirtualMachine = $DeviceDetails.IsVirtualMachine
            $CNLastOnlineTime  = $DeviceDetails.CNLastOnlineTime
            $CNLastOfflineTime = $DeviceDetails.CNLastOfflineTime
            $ResourceID = $DeviceDetails.ResourceID
        }catch{
            Write-Host "Warning: One of the standard details could not be determined. This probably means the machine is not in SCCM." -ForegroundColor cyan
            $IsClient = $null
            $CMUserName = $null
            $LastStatusMessage = $null
            $LastSoftwareScan  = $null
            $LastPolicyRequest = $null
            $LastDDR = $null
            $LastClientCheckTime = $null
            $LastActiveTime  = $null
            $IsVirtualMachine = $null
            $CNLastOnlineTime  = $null
            $CNLastOfflineTime = $null
            $ResourceID = $null
        }

        #Get Expanded SCCM WMI Device Details

        try{
            $ExpandedDeviceDetails = Get-DGMSCCMWMIHardwareDetails -ResourceID $ResourceID -ProviderMachineName $ProviderMachineName -Sitecode $SiteCode
            $SerialNumber = $ExpandedDeviceDetails.SMS_G_System_PC_BIOS.SerialNumber
            $BIOSVersion = $ExpandedDeviceDetails.SMS_G_System_PC_BIOS.SMBIOSBIOSVersion
            $Manufacturer = $ExpandedDeviceDetails.SMS_G_System_COMPUTER_SYSTEM.Manufacturer
            $Model = $ExpandedDeviceDetails.SMS_G_System_COMPUTER_SYSTEM.Model
            $Architecture = $ExpandedDeviceDetails.SMS_G_System_COMPUTER_SYSTEM.SystemType
            $MACAddresses = $ExpandedDeviceDetails.SMS_R_System.MACAddresses
        }catch{
            Write-Host "Warning: One of the expanded details could not be determined. This probably means the machine is not in SCCM." -ForegroundColor Yellow
            $SerialNumber = $null
            $BIOSVersion = $null
            $Manufacturer = $null
            $Model = $null
            $Architecture = $null
            $MACAddresses = $null
        }

        try{
            #Get User Device Affinity Details
            $DGMSCCMUsersOfDevice = Get-DGMSCCMUsersOfDevice -Hostname $Name -ProviderMachineName $ProviderMachineName -Sitecode $SiteCode
        }catch{
            $DGMSCCMUsersOfDevice = $null
        }

        #Get User Details
        
        if ($PredeterminedUser){
            try{$DGMUserDetails = Get-DGMUserDetails -UserName $PredeterminedUser}catch{$DGMUserDetails = $null}
            $PrimaryUser = $DGMUserDetails.DisplayName
            $Title = $DGMUserDetails.Title
            $Department = $DGMUserDetails.Department
            $EmailAddress = $DGMUserDetails.EmailAddress
            $UserSource = "Predetermined User"
        }
        elseif($SME1){
            try{$DGMUserDetails = Get-DGMUserDetails -DisplayName $SME1}catch{$DGMUserDetails = $null}
            $PrimaryUser = $DGMUserDetails.DisplayName
            $Title = $DGMUserDetails.Title
            $Department = $DGMUserDetails.Department
            $EmailAddress = $DGMUserDetails.EmailAddress
            $UserSource = "AD SME1 Attribute"
        }elseif ($CMUserName){
            try{$DGMUserDetails = Get-DGMUserDetails -UserName $CMUserName}catch{$DGMUserDetails = $null}
            $PrimaryUser = $DGMUserDetails.DisplayName
            $Title = $DGMUserDetails.Title
            $Department = $DGMUserDetails.Department
            $EmailAddress = $DGMUserDetails.EmailAddress
            $UserSource = "SCCM Hardware Association"
        }elseif ($DGMSCCMUsersOfDevice){
            $Users = $DGMSCCMUsersOfDevice
            foreach ($user in $users){ 
                $i++
                try{$DGMUserDetails = Get-DGMUserDetails -UserName $user}catch{$DGMUserDetails = $null}
                if ($i -eq 1){
                    $PrimaryUser = $DGMUserDetails.DisplayName
                    $Title = $DGMUserDetails.Title
                    $Department = $DGMUserDetails.Department
                    $EmailAddress = $DGMUserDetails.EmailAddress
                    $UserSource = "SCCM User-Device Affinity"
                
                 }
                else{
                    $SecondaryUser = $DGMUserDetails.DisplayName
                    $OtherUsers=$OtherUsers+$SecondaryUser+", "
            
                }
            $UserSource = "SCCM User-Device Affinity"
            }
        }elseif($Description -and ($DGMUserDetails = Get-DGMUserDetails -Description $Description)){
            #$DGMUserDetails = Get-DGMUserDetails -Description $Description
            $PrimaryUser = $DGMUserDetails.DisplayName
            $Title = $DGMUserDetails.Title
            $Department = $DGMUserDetails.Department
            $EmailAddress = $DGMUserDetails.EmailAddress
            $UserSource = "AD Description"
        }else{
            $OtherUsers = $null
            $PrimaryUser = $null
            $DisplayName = $null
            $Title = $null
            $Department = $null
            $EmailAddress = $null
            $UserSource = $null
        }

         $output = [PSCustomObject] @{
            'Name' = $Name;
            'Pingable' = $_.Pingable
            'IP Address' = $IPv4Address;
            'AD Enabled' = $Enabled;
            'RDP Enabled' = $RDPEnabled;
            'Connect to c$' = $PathEnabled;
            'VirtualCNO' = $VirtualCNO;
            'SCCM Client' = $IsClient;
            'Primary User' = $PrimaryUser;
            'User Source' = $UserSource;
            'User Title' = $Title;
            'User Department'  = $Department;
            'User E-Mail' = $EmailAddress;
            'Other Users' = $OtherUsers;
            'Operating System' = $OperatingSystem;
            'Manufacturer' = $Manufacturer;
            'Model' = $Model;
            'Serial Number' = $SerialNumber;
            'BIOS Version' = $BIOSVersion;
            '$Architecture' = $Architecture;
            'MAC Addresses' = $MACAddresses;
            'AD Description' = $Description;
            'DHCP Scope Location' = $IPv4ScopesDescription;
            'AD Creation Date' = $Created;
            #'Subnet' = $IPv4Scope;
            'Canonical Name' = $CanonicalName
            'Virtual Machine' = $IsVirtualMachine
            'Last Time Online' = $CNLastOnlineTime;
            'Last Time Offline' = $CNLastOfflineTime;
            'MW 1' = $MW1;
            'MW 2' = $MW2;
            'SME 1' = $SME1;
            'SME 2' = $SME2;
            'Predetermined User' = $PredeterminedUser;
        }

        $finalresults+=$output

       }

    #Export to CSV if chosen
        if ($csvoutputcheck){
            Write-Host =========================================== -ForegroundColor Cyan
            Write-Host CSV Output Results -ForegroundColor Cyan
            Write-Host =========================================== -ForegroundColor Cyan

            New-DGMCSVOut -csvoutputpath $csvoutput -arrayforoutput $finalresults 
        }

        #Remove-PSDrive "$SiteCode:"
        return $finalresults
        
}

Function Get-DGMUserDetails{
    [CmdletBinding()]
    [Alias()]
    Param
    (
        [Parameter(Mandatory=$false,ValueFromPipeline=$false,Position=0)] 
        [String]$UserName,
        [Parameter(Mandatory=$false,ValueFromPipeline=$false,Position=1)] 
        [String]$DisplayName,
        [Parameter(Mandatory=$false,ValueFromPipeline=$false,Position=2)] 
        [String]$Description
    )
    #Check for what arguments were passed
    if([bool]($MyInvocation.BoundParameters.Keys -match 'UserName')){
        #Remove Domain Name if found
        $UserName = $UserName -creplace '^[^\\]*\\', ''

        try{
            $UserObject = Get-ADUser $Username -Properties DisplayName,Title,Department,EmailAddress | Select -First 1

            $output = [PSCustomObject] @{
                'DisplayName' = $UserObject.DisplayName
                'Title' = $UserObject.Title
                'Department' = $UserObject.Department
                'EmailAddress' = $UserObject.EmailAddress;
            }

            return $output

        }catch{
            return $False
        }
    }
    #Check for what arguments were passed
    if([bool]($MyInvocation.BoundParameters.Keys -match 'DisplayName')){
        try{
            $UserObject = Get-ADUser -filter ('DisplayName -like "*' + $DisplayName + '*"') -Properties DisplayName,Title,Department,EmailAddress | Select -First 1

            $output = [PSCustomObject] @{
                'DisplayName' = $UserObject.DisplayName
                'Title' = $UserObject.Title
                'Department' = $UserObject.Department
                'EmailAddress' = $UserObject.EmailAddress;
            }

            return $output

        }catch{
            return $False
        }
    }

    #Check for what arguments were passed
    if([bool]($MyInvocation.BoundParameters.Keys -match 'Description')){
        
        #Replace bad charecters in the description with a space
        $description = $description -replace '-',' '

        #Split the string into an array
        $descriptionarray = $description -split " "

        #Clear Output
        $output = $null

        #Loop Through Every Sequential Two Word Combination in the description (looking for a first and last name) and see if there is a user associated to it in AD
        for ($i=0; $i -lt $descriptionarray.Length-1; $i++) {
            $DisplayName = $descriptionarray[$i]+" "+$descriptionarray[$i+1]
            
            $UserObject = Get-ADUser -filter ('DisplayName -like "*' + $DisplayName + '*"') -Properties DisplayName,Title,Department,EmailAddress | Select -First 1

            $output = [PSCustomObject] @{
                'DisplayName' = $UserObject.DisplayName
                'Title' = $UserObject.Title
                'Department' = $UserObject.Department
                'EmailAddress' = $UserObject.EmailAddress;
                }
            }

        if ($output.DisplayName -ne $null) {return $output}else{return $false}
    }
}

Function Get-DGMSCCMUsersOfDevice{

    [CmdletBinding()]
    [Alias()]
    Param
    (
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$Hostname,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$ProviderMachineName,
        [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateLength(3,3)]
        [String]$Sitecode
    )

    #Connect to SCCM
    # Import the ConfigurationManager.psd1 module
    $module = "$($ENV:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1"
    if((Get-Module ConfigurationManager) -eq $null) {
        Write-Host Importing $module ...
        Import-Module $module -Force
    }

    # Connect to the site's drive if it is not already present
    if((Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue) -eq $null) {
        $NewPSDrive = New-PSDrive -Name $SiteCode -PSProvider CMSite -Root $ProviderMachineName
    }

    # Set the current location to be the site code.
    Set-Location "$($SiteCode):\"

    $CMUserDeviceAffinity = Get-CMUserDeviceAffinity -DeviceName $Hostname
    $users = $CMUserDeviceAffinity.UniqueUserName

    return $users
}

Function Get-DGMSCCMDeviceDetails{

    [CmdletBinding()]
    [Alias()]
    Param
    (
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$Hostname,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$ProviderMachineName,
        [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateLength(3,3)]
        [String]$Sitecode
    )

    #Connect to SCCM
    # Import the ConfigurationManager.psd1 module
    $module = "$($ENV:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1"
    if((Get-Module ConfigurationManager) -eq $null) {
        Write-Host Importing $module ...
        Import-Module $module -Force
    }

    # Connect to the site's drive if it is not already present
    if((Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue) -eq $null) {
        $NewPSDrive = New-PSDrive -Name $SiteCode -PSProvider CMSite -Root $ProviderMachineName
    }

    # Set the current location to be the site code.
    Set-Location "$($SiteCode):\"
    try{
        $DeviceProperties = Get-CMDevice -Name $Hostname
        return $DeviceProperties
    }catch{
        return $FALSE
    }

}

function Get-DGMSearchQueryComputers{
    [CmdletBinding()]
    [Alias()]
    Param
    (
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$ProviderMachineName,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateLength(3,3)]
        [String]$Sitecode
    )

    
    #Connect to SCCM
    # Import the ConfigurationManager.psd1 module
    $module = "$($ENV:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1"
    if((Get-Module ConfigurationManager) -eq $null) {
        Write-Host Importing $module ...
        Import-Module $module -Force
    }

    # Connect to the site's drive if it is not already present
    if((Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue) -eq $null) {
        $NewPSDrive = New-PSDrive -Name $SiteCode -PSProvider CMSite -Root $ProviderMachineName
    }

    # Set the current location to be the site code.
    Set-Location "$($SiteCode):\"

    Write-Host "[1] Search for single computer name"
    Write-Host "[2] Search by wildcard computer name"
    Write-Host "[3] Search by description"
    Write-Host "[4] Search by Operating System"
    Write-Host "[5] Search by User Name (DOMAIN\USER)"
    Write-Host "[6] Import CSV (with PCs as 'Name' column)"
    Write-Host "[7] Import CSV (with Users as 'PredeterminedUser' column)"

    do {
        try {$numOk = $true; [int]$GetMyANumber = Read-host "Selection"}
        catch {$numOK = $false}}
    until (($GetMyANumber -ge 1 -and $GetMyANumber -le 7) -and $numOK)

    $validcharacters = "^[a-zA-Z0-9\\\s-_@]+$"
    do {
        try {$stringOk = $true; [string]$query = Read-host "Enter search query (A-Z, a-z, 0-9,\,-,_,@ only)"}
        catch {$stringOk = $false}}
    until (($query -match $validcharacters) -and $stringOk)

    switch ($GetMyANumber) 
        { 
            1 {$finalresults = Get-ADComputer -filter {name -eq $query}}
            2 {$query = "*"+$query;$query = $query+"*";$finalresults = Get-ADComputer -filter {name -like $query}}
            3 {$query = "*"+$query;$query = $query+"*";$finalresults = Get-ADComputer -filter {description -like $query}}
            4 {$query = "*"+$query;$query = $query+"*";$finalresults = Get-ADComputer -filter {OperatingSystem -like $query}}
            5 {$finalresults = Get-CMUserDeviceAffinity -UserName $query | select @{name ="Name";e={$_.ResourceName}}}
            6 {$csvinputfile = Get-DGMFileName -filter "CSV";$csvinput = import-csv -Path $csvinputfile; $finalresults = $csvinput}
            7 {$csvinputfile = Get-DGMFileName -filter "CSV";$csvinput = import-csv -Path $csvinputfile; 
                $finalresults = @()
                foreach ($line in $csvinput){
                    $usercomputers = Get-CMUserDeviceAffinity -UserName $line.PredeterminedUser | Sort-Object -Property Sources | select @{name ="Name";e={$_.ResourceName}},Sources
                    foreach ($usercomputer in $usercomputers){
                        $output = [PSCustomObject] @{
                                    'Name' = $usercomputer.Name;
                                    'PredeterminedUser' = $line.PredeterminedUser;
                                    'Sources' = $usercomputer.Sources;
                                }
                        $finalresults+=$output
                   }
                }
                 $finalresults | Out-GridView -Title "User Affinity Preview. Still processing..."
              }
        }

    return $finalresults

}

function Get-DGMOnlineComputers{

    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [int] $threads,
        [Array] $ComputerList
    )
    
    Write-Host ============================================ -ForegroundColor Cyan
    Write-Host Pinging Computers and Building Table -ForegroundColor Cyan
    Write-Host ============================================ -ForegroundColor Cyan
    
    $finalresults = @()

    if ($ComputerList.Length -gt 0){
        Write-Host "Pinging"($ComputerList.length)"computers in $threads threads."
        $finalresults = $ComputerList | Where-ParallelObject -Filter {Test-Connection -ComputerName $_.Name -Quiet -Count 1} -Threads $threads -ProgressBar -progressBartext "Buildng Computer Details"
        return $finalresults
    }else{
        Write-Host "No Pingable Computers were found."
        return $false
    }
}

function Get-DGMRDPComputers{

    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [int] $threads,
        [Array] $ComputerList
    )
    
    Write-Host ============================================ -ForegroundColor Cyan
    Write-Host Testing RDP on Computers and Building Table -ForegroundColor Cyan
    Write-Host ============================================ -ForegroundColor Cyan
    
    $finalresults = @()

    if ($ComputerList.Length -gt 0){
        Write-Host "RDP Check on"($ComputerList.length)"computers in $threads threads."
        $finalresults = $ComputerList | Where-ParallelObject -Filter {Test-NetConnection -ComputerName $_.Name -CommonTCPPort RDP -InformationLevel Quiet -WarningAction SilentlyContinue} -Threads $threads -ProgressBar -progressBartext "Buildng RDP Return Details"
        return $finalresults
    }else{
        Write-Host "No RDP Enabled Computers were found."
        return $false
    }
}

function Get-DGMTestPath{

    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [int] $threads,
        [Array] $ComputerList,
        [String]$path
    )
    
    Write-Host ============================================ -ForegroundColor Cyan
    Write-Host Testing Path on Computers and Building Table -ForegroundColor Cyan
    Write-Host ============================================ -ForegroundColor Cyan
    
    $finalresults = @()

    if ($ComputerList.Length -ge 0){
        Write-Host "Testing Path $path on"($ComputerList.length)"computers in $threads threads."
        $finalresults = $ComputerList | Where-ParallelObject -Filter {Test-Path $('filesystem::\\'+$_.Name+'\'+$path)} -Threads $threads -ProgressBar -progressBartext "Building Test-Path $path Details"
        return $finalresults
    }else{
        Write-Host "No Computers Returned a valid path."
        return $false
    }
}

function New-DGMCSVOut{

    param(
            [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
            [String] $csvoutputpath,
            [array] $arrayforoutput
        )

    try{
        $arrayforoutput | export-csv $csvoutputpath -notypeinformation
        Write-Host "CSV Export`: CSV Created at $csvoutputpath"
    }catch{
        Write-Host "CSV Export`: CSV Could NOT be Created at $csvoutputpath" -ForegroundColor Red
    }


}

function Where-ParallelObject {
    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)] $input,
        [ScriptBlock] $Filter,
        [int] $threads,
        [switch] $progressBar,
        [String] $progressBartext
    )

    $inputQueue = [System.Collections.Queue]::Synchronized( (New-Object System.Collections.Queue) )
    $results = [System.Collections.Queue]::Synchronized( (New-Object System.Collections.Queue) )

    $sessionstate = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
    $sessionstate.Variables.Add(
        (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry('inputQueue', $inputQueue, $null))
    )
    $sessionstate.Variables.Add(
        (New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry('results', $results, $null))
    )

    $runspacepool = [runspacefactory]::CreateRunspacePool(1, $threads, $sessionstate, $Host)
    $runspacepool.Open()

    foreach ($object in $input) {
        $inputQueue.Enqueue($object)
    }

    $jobs = @()

    $sbpre = '
        while($inputQueue.Count -gt 0) {
            $_ = $inputQueue.Dequeue();
            if('
    $sbpost = ') 
            {
                $results.Enqueue($_);    
            }
        }
    '

    $sb = [ScriptBlock]::Create($sbpre + $Filter.toString() + $sbpost)

    1..$threads | % {
        $job = [PowerShell]::Create().AddScript($sb)
        $job.RunspacePool = $runspacepool
        $jobs += New-Object PSObject -Property @{
            Job = $job
            Result = $job.BeginInvoke()
        }
    }

    do {
        if($progressBar.IsPresent) 
        {
            Write-Progress -Activity ($progressBartext+" " +$input.Count+ " Objects") -status ("" + $($results.Count) + " complete.") -percentComplete ( ($results.Count) / $input.Count * 100) 
        }
        Start-Sleep -Seconds 1
    } while ( $jobs.Result.IsCompleted -contains $false)

    foreach ($job in $jobs) {
        $job.Job.EndInvoke($job.Result)
    }
    $runspacepool.Close()
    $runspacepool.Dispose()

    return $results.ToArray()

}

function Get-DGMSCCMWMIHardwareDetails{
    [CmdletBinding()]
    [Alias()]
    Param
    (
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$ResourceID,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$ProviderMachineName,
        [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateLength(3,3)]
        [String]$Sitecode
    )
    #Change these to match your Server Name and Site Code
    $SCCMnameSpace = "root\SMS\SITE_$Sitecode"

    #Query WMI to get the 'Computer System Product' information
    $qry = "select * from SMS_R_System inner join SMS_G_System_COMPUTER_SYSTEM on SMS_G_System_COMPUTER_SYSTEM.ResourceID = SMS_R_System.ResourceId inner join SMS_G_System_PC_BIOS on SMS_G_System_PC_BIOS.ResourceID = SMS_R_System.ResourceId where ResourceID = '$ResourceID'"
    
    try{
        $objComputerSystemProduct = Get-WmiObject -ComputerName $ProviderMachineName -Namespace $SCCMnameSpace -Query $qry
        return $objComputerSystemProduct
    }catch{
        return $False
    }
}

Function Get-DGMFileName(){   
    [CmdletBinding()]
    [Alias()]
    Param
    (
        [Parameter(Position=0,Mandatory=$false,ValueFromPipeline=$true)]
        [String]$initialDirectory,
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$filter
    )
    [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") |
    Out-Null

    $OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
    $OpenFileDialog.initialDirectory = $initialDirectory
    $OpenFileDialog.filter = "$filter files (*.$filter)| *.$filter"
    $OpenFileDialog.ShowDialog() | Out-Null
    $OpenFileDialog.filename
}

function Invoke-DGMThreadedComputerDetails{
    [CmdletBinding()]
    [Alias()]
    Param
    (
    )
    Write-Host "============================================"
    Write-Host "Get Threaded Computer Details"
    Write-Host "============================================"
    Write-Host "Author: dmaiolo"
    Write-Host "Instructions: Choose a CSV file where the column of computer names is 'Name'"
    $csvinput = Get-DGMFileName -filter "CSV"
    Get-DGMThreadedComputerDetails -ProviderMachineName SCCMSERVERNAME -Sitecode SITE -domaincontroller DOMAINCONTROLLER01 -csvinput $csvinput
}

Overview

This is a “Magic Offline Imaging Jumpdrive” I put together that can be used for OEM imaging or offline imaging where you still need to join PC’s to the domain but don’t have access to the network when the computer is being imaged. The idea is this “Magic Jumpdrive” would allow a trusted party to image your equipment without needing access to your network, broadening where and when a machine could be imaged.

Process

Create the Magic Offline Imaging Jumpdrive SCCM Package

First, download the Magic Offline Imaging Jumpdrive and unzip the contents


Magic Offline Imaging Jumpdrive

Create a new source location for content in your SCCM data directory, and all of the files found within the OFFLINE_PACKAGE_CONTENTS folder from the file you just downloaded:

Next, create an SCCM Package (or application if you prefer) and add two programs for each of the CMD files. The command line for each program only needs to point to the name of the file:

Setup the Magic Offline Imaging Jumpdrive OS Task Sequence

Next, choose the task sequence in SCCM that you would like to be available offline and copy it as a new Task sequence with “(OFFLINE)” in the name appended after it, such as “Windows 10 (OFFLINE)”.

Now, add two steps to this task sequence after the image has been applied, yet before anything you want installed on a “Domain Joined” machine. Point each step to the package you created earlier, with the STEP 1 and 2 in sequence:

Create the Magic Offline Imaging Jumpdrive

Now, create an offline Jumpdrive of this OFFLINE task sequence using the built in Create Task Sequence Media SCCM task sequence wizard:

Put your Jumpdrive aside as we’ll need it again in a few moments.

Creating the Offline Computer Provisioning Files

Now, within the file you download, inside the ADMINISTRATIVE_TOOLS directory modify the contents of the Add_Offline_Machine.cmd file to include the OU and Domain you want the machine placed in:


djoin /provision /domain "fqdn.company.com" /machine "%computerName%" /savefile .\%computerName%.txt /machineou "OU=Offline Domain Join,OU=Workstations,OU=con,DC=corp,DC=contoso,DC=com"

and the security group you want the machine placed in:


dsmod group "CN=ISE - Offline Domain Join,OU=Your Special Offline Security Group,OU=Security Groups,OU=con,DC=corp,DC=contoso,DC=com" -addmbr "CN=%computerName%,OU=Offline Domain Join,OU=Workstations,OU=cor,DC=corp,DC=contoso,DC=com"

If you don’t care to have the machine placed in a security group, just REM out this line. However, I recommend you do add it to a special security group with restricted permissions. You can then remove it from this group later once you’ve determined the computer is in safe hands.

Now launch the tool Add_Offline_Machine.cmd which will pre-provision offline domain objects for a serious of computers. These will be the names of the computers you want to be available to offline domain join:

You’ll notice two things happened. One, you’ll find a new COMPUTERNAME.txt file in the same directory you ran the tool. This is the offline provisioning file, and you’ll want to copy it to the ROOT of the Jumpdrive:

Second, you’ll notice a computer object was created inside the OU you specified earlier. This .txt file and computer object are a special pair. Our .txt offline provision file has a trusted key inside of it that Active Directory will recognize and trust, and associate to this computer object later on during the process. It’s all automated, so you don’t need to worry.

Booting and Imaging the Offline Computer with the Magic Offline Imaging Jumpdrive

Now comes the fun part. Take your Jumpdrive to a computer that is not connected to the network and boot it from the Jumpdrive. Image the computer in the normal fashion. Later on in the process, you’ll be prompted with a wizard where you can choose the Offline Provision File you created earlier:

This list is generated from all of the offline provisioning .txt files you added to the root of the Jumpdrive earlier. Once you select a file, the computer will join the domain as that name, even when there is no network access. That’s the magic part! Also, the file will be renamed from .txt to .old, indicating it has been used so the wizard does not make it available again the next time the Jumpdrive is used.

Joining the Domain

When the computer connects to the corporate network, the special key/AD Computer object pair will be linked, and the computer is joined to the domain as that computer object.

Once you have confirmed the computer is in good hands, the computer can be placed into a proper Security Group where it would get the standard security policies.

Administrative Tool: Add_Offline_Machine.cmd


echo off
color 9F
cls
echo ==============================================================
echo Offline Domain Join Tool (dmaiolo v2017-04-28)
echo ==============================================================
echo.
echo This tool is used to add a computer object that can be used
echo during an offline domain join for purposes of imaging OEM equipment
echo when not joined to the network.
echo.
SET /P computerName=[Enter Hostname To Add to Offline Domain Join:]
REM Set your OU below where you want the computers placed. For security, you could stick these in a stagging OU that only allows access to resources once the machine has been approved by an administrator
djoin /provision /domain "fqdn.company.com" /machine "%computerName%" /savefile .\%computerName%.txt /machineou "OU=Offline Domain Join,OU=Workstations,OU=con,DC=corp,DC=contoso,DC=com"
echo Adding %computerName% to Jump Drive Save File...
echo Adding %computerName% to Security Group...
dsmod group "CN=ISE - Offline Domain Join,OU=Your Special Offline Security Group,OU=Security Groups,OU=con,DC=corp,DC=contoso,DC=com" -addmbr "CN=%computerName%,OU=Offline Domain Join,OU=Workstations,OU=cor,DC=corp,DC=contoso,DC=com"
pause

Offline Join Tool: Choose_Machine_Join_File_STEP1.cmd


color 9f
@echo off
setlocal enabledelayedexpansion
set mediaroot=d:
set djoinfile=CURRENT_OFFLINE_MACHINE.DJOIN

:START
cls
echo ===========================================================================
echo JOIN MACHINE TO DOMAIN (OFFLINE) (v20160413 dmaiolo)
echo ===========================================================================
if exist %mediaroot%\%djoinfile% (
    GOTO END
) else (
    GOTO CHOOSEFILE
)
:CHOOSEFILE
if exist %mediaroot%\*.txt (
    GOTO CHOOSEFILESTART
) else (
    echo ERROR! No Domain Join files were found on the media root.
    echo Please add a domain join file using the djoin.exe command and try and again.
    echo This process will continue to look for this file every time you press any key.
    echo To bypass this entire process presss CTRL+C. You if you do, this computer will
    echo not join the domain.
    pause
    GOTO START
)
:CHOOSEFILESTART
echo Choose the the file associated to this machine from the list below. If you
echo do not see your machine file listed, please contact the helpdesk to have
echo it created, and then add it to the root of this installation media.
echo -

set count=0
set "choice_options="

for /F "delims=" %%A in ('dir /a:-d /b %mediaroot%\*.txt') do (
    REM Increment %count% here so that it doesn't get incremented later
    set /a count+=1

    REM Add the file name to the options array
    set "options[!count!]=%%A"

    REM Add the new option to the list of existing options
    set choice_options=!choice_options!!count!
)

for /L %%A in (1,1,!count!) do echo [%%A]. !options[%%A]!
echo -
choice /D 1 /T 60 /c:!choice_options! /n /m "Enter Number From Above (Option 1 Chosen in 60 Seconds): "

set var1=!options[%errorlevel%]!
echo %var1% > %mediaroot%\%djoinfile%
set /p var1=<%mediaroot%\%djoinfile%

choice /D y /c yn /T 60  /n /m "Proceed With %var1%? (y/n) (y Chosen in 60 Seonds): "
if %errorlevel%==1 (GOTO END) else GOTO CHOOSEFILE
:END

Offline Join Tool: Choose_Machine_Join_File_STEP1.cmd


color 9f
@echo off
setlocal enabledelayedexpansion
set mediaroot=d:
set djoinfile=CURRENT_OFFLINE_MACHINE.DJOIN
cls
set /p var2=<%mediaroot%\%djoinfile%
echo ===========================================================================
echo JOIN MACHINE TO DOMAIN (OFFLINE) (v20160413 dmaiolo) STEP 2
echo ===========================================================================
if exist %mediaroot%\%djoinfile% (
    GOTO STARTJOIN
) else (
    GOTO NOFILEFOUND
)
:STARTJOIN
echo Joining %var2% to Domain...
djoin /requestODJ /loadfile %mediaroot%\%var2% /windowspath %systemroot% /localos
echo Removing %var2% from the future list of options...
rename %mediaroot%\%var2% *.old
del %mediaroot%\%djoinfile%
GOTO END
:NOFILEFOUND
echo No File Was Found
:END

Overview

Contained in this article are the tools to help you detect and remediate the Spectre and Meltdown security vulnerabilities. This remediation is accomplished via the following SCCM configuration items which I specifically developed for this purpose.

Contained in these configuration items are several PowerShell scripts and return values for the configuration items, and may cause a slight slowdown on the machine during the evaluation. The detection is based off the Get-SpeculationControlSettings function that was provided by Microsoft. It is recommended that the baseline and associated PowerShell scripts are instructed to run once a day. You simply need to download these and import them into SCCM, then assign them to a baseline for deployment.

The end goal is twofold:

  • Provide the detailed data that is required to for remediation of these vulnerabilities
  • Provide the standard reporting on what devices remain vulnerable, and in which phase they are being remediated.

Configuration Baseline: Direct Detection

Use these configuration items to detect the vulnerabilities directly. This is the most useful baseline for determining your Spectre / Baseline compliance

  • CI.Meltdown-Spectre.CVE-2017-5754.mitigated
  • CI.Meltdown-Spectre.CVE-2017-5715.mitigated
  • CI.Meltdown-Spectre.CVE-2017-5753.mitigated.Chrome
  • CI.Meltdown-Spectre.CVE-2017-5753.mitigated.Firefox
  • CI.Meltdown-Spectre.CVE-2017-5753.mitigated.IE
  • CI.Meltdown-Spectre.AVCompatibility


Download Spectre Meltdown Direct Configuration Items

Direct Detection Reporting

When deployed as a baseline to your environment, you are presented with data that reports your overall compliance for the Meltdown / Spectre vulnerabilities. For a computer to be compliant, it must at least satisfy each of these six items. To really summarize what is being tested, it is important each computer is remdiated for CVE-2017-5715, CVE-2017-5754, CVE-2017-5753 and has the proper version of Antivirus.

Sample Report

cid:image003.png@01D3892D.E4C1DC00

cid:image001.jpg@01D38930.AA0A3EB0

Test Name

% Compliance

Type

Version

Total Clients

Compliant

Non-compliant

Failed

Remediated

Not-Applicable

Not-Detected

Unknown

CI.Meltdown-Spectre.CVE-2017-5754.mitigated

0.12%

OS

2

1607

2

1384

2

0

0

0

219

CI.Meltdown-Spectre.CVE-2017-5715.mitigated

0.00%

OS

2

1607

0

1386

2

0

0

0

219

CI.Meltdown-Spectre.CVE-2017-5753.mitigated.Chrome

83.76%

OS

4

1607

1346

39

3

0

0

0

219

CI.Meltdown-Spectre.CVE-2017-5753.mitigated.Firefox

85.44%

OS

5

1607

1373

11

4

0

0

0

219

CI.Meltdown-Spectre.CVE-2017-5753.mitigated.IE

0.68%

OS

5

1607

11

1375

2

0

0

0

219

CI.Meltdown-Spectre.Cisco.Amp.Equals.5.1.13.10483.or.6.0.5.10636

71.81%

OS

6

1607

1154

234

0

0

0

0

219

Configuration Baseline: Full Detection

Use these configuration items to detect all items associated with the vulnerabilities. This is the most useful baseline to supply to your IT staff so they have all of the relevant data to assist in remediation.

  • CI.Meltdown-Spectre.AVCompatibility
  • CI.Meltdown-Spectre.BTIDisabledByNoHardwareSupport
  • CI.Meltdown-Spectre.BTIDisabledBySystemPolicy
  • CI.Meltdown-Spectre.BTIHardwarePresent
  • CI.Meltdown-Spectre.BTIWindowsSupportEnabled
  • CI.Meltdown-Spectre.BTIWindowsSupportPresent
  • CI.Meltdown-Spectre.Cisco.Amp.Equals.5.1.13.10483.or.6.0.5.10636
  • CI.Meltdown-Spectre.CVE-2017-5715.mitigated
  • CI.Meltdown-Spectre.CVE-2017-5753.mitigated.Chrome
  • CI.Meltdown-Spectre.CVE-2017-5753.mitigated.Edge
  • CI.Meltdown-Spectre.CVE-2017-5753.mitigated.Firefox
  • CI.Meltdown-Spectre.CVE-2017-5753.mitigated.IE
  • CI.Meltdown-Spectre.CVE-2017-5754.mitigated
  • CI.Meltdown-Spectre.isDocker
  • CI.Meltdown-Spectre.isHyperV
  • CI.Meltdown-Spectre.isTerminalServer
  • CI.Meltdown-Spectre.KVAShadowPcidEnabled
  • CI.Meltdown-Spectre.KVAShadowRequired
  • CI.Meltdown-Spectre.KVAShadowWindowsSupportEnabled
  • CI.Meltdown-Spectre.KVAShadowWindowsSupportPresent
  • CI.Meltdown-Spectre.MinVmVersionForCpuBasedMitigations.Less.Than.6
  • CI.Meltdown-Spectre.OSMitigationRegKeySet
  • CI.Meltdown-Spectre.OSMitigationRegKeySet.FeatureSettingsOverride
  • CI.Meltdown-Spectre.OSMitigationRegKeySet.FeatureSettingsOverrideMask


Download Spectre Meltdown Full Configuration Items

Configuration Baseline

I suggest including the provided configuration items into the following two baselines:

  • CB.Meltdown-Spectre.Direct.Compliance
    • Failing compliance for this baseline indicates the computer is at risk.
  • CB.Meltdown-Spectre.Full.Compliance
    • Failing compliance for this baseline does not necessarily indicate risk, but provides all the reporting metrics that will be required for the remediation phase.

Sample Direct Report:

cid:image006.jpg@01D38897.D94CE1D0

Sample Full Report:

cid:image008.jpg@01D38897.D94CE1D0

Here is a sample of the baseline run on a machine and the type of data it gathers for CB.Meltdown-Spectre.Full.Compliance:

cid:image010.jpg@01D38897.D94CE1D0

Here is a sample of the baseline run on a machine and the type of data it gathers for CB.Meltdown-Spectre.Direct.Compliance:

cid:image011.jpg@01D38897.D94CE1D0

Mitigation Tactics

A Meltdown-Spectre vulnerability auto-remediation can be pushed to workstations to target any item, such as the OSMitigationRegKeySet. In this example, a collection of computers is targeted that fail the CI.Meltdown-Spectre.OSMitigationRegKeySet configuration item.

What Could This OSMitigationRegKeySet Fix Mitigate?

Enabling these mitigations may affect performance. The actual performance impact will depend on multiple factors, such as the specific chipset in your physical host and the workloads that are running. Microsoft recommends that you assess the performance impact for their environment and make necessary adjustments.

OSMitigationRegKeySet is true if the values for the registry key Memory Management are set as required,

i.e. FeatureSettingsOverride is 0 and FeatureSettingsOverrideMask is 3. OSMitigationRegKeySet is empty if the computer is a client.

An auto-remediation will create the following registry values if they are not present:

  • ‘HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management’ -PropertyType ‘DWORD’ -Value ‘0’  -Name ‘FeatureSettingsOverride’
  • ‘HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management’ -PropertyType ‘DWORD’ -Value ‘3’  -Name ‘FeatureSettingsOverrideMask’

Determinign Compliance

Compliance for this vulnerability can be tracked as follows:

cid:image002.jpg@01D38F87.A84E3150

cid:image004.jpg@01D38F87.A84E3150

Reporting Application

There is also a report I developed that stores the vulnerability check values locally on the computer for easy reporting purposes on the workstation itself. This creates values in the registry that can be easily viewed in the following registry location:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\MeltdownSpectreReport

A sample of the registry data the report produces:

The application as deployed through SCCM:

cid:image012.jpg@01D38897.D94CE1D0

PowerShell Function for Application

Run this as an SCCM application to return registry value results as shown above. This is a modification to the Get-SpeculationControlSettings function provided by Microsoft


  <#
.SYNOPSIS
    Query mitigation status of Meltdown and Spectre against one or multiple computers
.DESCRIPTION
    This script uses Get-SpeculationControlSettings (Microsoft) to get the mitigation status for Windows, 
    and extends the information with various registry keys, computer and software information to get a 
    broader picture. Also it uses Invoke-Parallel (RamblingCookieMonster) and Invoke-Command to obtain the 
    information from remote computers with speed.
.EXAMPLE
    PS C:\> .\MeltdownSpectreReport.ps1 -ComputerName computer01
    ComputerName                       : computer01
    Manufacturer                       : HP
    Model                              : HP Spectre x360 Convertible
    BIOS                               : F.47
    CPU                                : Intel(R) Core(TM) i7-6560U CPU @ 2.20GHz
    OperatingSystem                    : Microsoft Windows 10 Pro
    OSReleaseId                        : 1709
    isHyperV                           : True
    isTerminalServer                   : False
    isDocker                           : True
    CVE-2017-5754 mitigated            : True
    CVE-2017-5715 mitigated            : False
    CVE-2017-5753 mitigated in Edge    : True
    CVE-2017-5753 mitigated in IE      : True
    CVE-2017-5753 mitigated in Chrome  : False
    CVE-2017-5753 mitigated in Firefox : True
    BTIHardwarePresent                 : False
    BTIWindowsSupportPresent           : True
    BTIWindowsSupportEnabled           : False
    BTIDisabledBySystemPolicy          : False
    BTIDisabledByNoHardwareSupport     : True
    KVAShadowRequired                  : True
    KVAShadowWindowsSupportPresent     : True
    KVAShadowWindowsSupportEnabled     : True
    KVAShadowPcidEnabled               : True
    OSMitigationRegKeySet              :
    AVCompatibility                    : True
    MinVmVersionForCpuBasedMitigations : 2.0
    InstalledUpdates                   : {@{HotFixId=KB4048951; Description=Security Update; InstalledOn=15.11.2017 00:00:00; ComputerName=computer01},
                                        @{HotFixId=KB4049179; Description=Security Update; InstalledOn=05.11.2017 00:00:00; ComputerName=computer01},
                                        @{HotFixId=KB4051613; Description=Update; InstalledOn=09.11.2017 00:00:00; ComputerName=computer01}, @{HotFixId=KB4053577;
                                        Description=Security Update; InstalledOn=01.01.2018 00:00:00; ComputerName=computer01}...}
    Uptime                             : 15:01:18.3875647
    ExecutionDate                      : 06.01.2018
.EXAMPLE
    PS C:\> $ComputerName = Get-ADComputer -Filter * | Select-Object -ExpandProperty Name
    $Report = .\MeltdownSpectreReport.ps1 -ComputerName $ComputerName
    $Report | ConvertTo-Csv -NoTypeInformation -Delimiter ',' | Out-File C:\report.csv
    $Report | Out-GridView
.EXAMPLE
    PS C:\> $ComputerName = Get-Content $env:USERPROFILE\Desktop\servers.txt
    .\MeltdownSpectreReport.ps1 -ComputerName $ComputerName -ErrorAction SilentlyContinue | 
    Export-Csv -Path $env:USERPROFILE\Desktop\servers.txt -NoTypeInformation
.NOTES
    Author: VRDSE
    Version: 0.4.2
#>
[CmdletBinding()]
param(
    # Specify remote computers to query against. If not set, local computer is queried.
    [Parameter()]
    [string[]]
    $ComputerName
)
function Invoke-Parallel {
    <#
    .SYNOPSIS
        Function to control parallel processing using runspaces

    .DESCRIPTION
        Function to control parallel processing using runspaces

            Note that each runspace will not have access to variables and commands loaded in your session or in other runspaces by default.
            This behaviour can be changed with parameters.

    .PARAMETER ScriptFile
        File to run against all input objects.  Must include parameter to take in the input object, or use $args.  Optionally, include parameter to take in parameter.  Example: C:\script.ps1

    .PARAMETER ScriptBlock
        Scriptblock to run against all computers.

        You may use $Using: language in PowerShell 3 and later.

            The parameter block is added for you, allowing behaviour similar to foreach-object:
                Refer to the input object as $_.
                Refer to the parameter parameter as $parameter

    .PARAMETER InputObject
        Run script against these specified objects.

    .PARAMETER Parameter
        This object is passed to every script block.  You can use it to pass information to the script block; for example, the path to a logging folder

            Reference this object as $parameter if using the scriptblock parameterset.

    .PARAMETER ImportVariables
        If specified, get user session variables and add them to the initial session state

    .PARAMETER ImportModules
        If specified, get loaded modules and pssnapins, add them to the initial session state

    .PARAMETER Throttle
        Maximum number of threads to run at a single time.

    .PARAMETER SleepTimer
        Milliseconds to sleep after checking for completed runspaces and in a few other spots.  I would not recommend dropping below 200 or increasing above 500

    .PARAMETER RunspaceTimeout
        Maximum time in seconds a single thread can run.  If execution of your code takes longer than this, it is disposed.  Default: 0 (seconds)

        WARNING:  Using this parameter requires that maxQueue be set to throttle (it will be by default) for accurate timing.  Details here:
        http://gallery.technet.microsoft.com/Run-Parallel-Parallel-377fd430

    .PARAMETER NoCloseOnTimeout
        Do not dispose of timed out tasks or attempt to close the runspace if threads have timed out. This will prevent the script from hanging in certain situations where threads become non-responsive, at the expense of leaking memory within the PowerShell host.

    .PARAMETER MaxQueue
        Maximum number of powershell instances to add to runspace pool.  If this is higher than $throttle, $timeout will be inaccurate

        If this is equal or less than throttle, there will be a performance impact

        The default value is $throttle times 3, if $runspaceTimeout is not specified
        The default value is $throttle, if $runspaceTimeout is specified

    .PARAMETER LogFile
        Path to a file where we can log results, including run time for each thread, whether it completes, completes with errors, or times out.

    .PARAMETER AppendLog
        Append to existing log

    .PARAMETER Quiet
        Disable progress bar

    .EXAMPLE
        Each example uses Test-ForPacs.ps1 which includes the following code:
            param($computer)

            if(test-connection $computer -count 1 -quiet -BufferSize 16){
                $object = [pscustomobject] @{
                    Computer=$computer;
                    Available=1;
                    Kodak=$(
                        if((test-path "\\$computer\c$\users\public\desktop\Kodak Direct View Pacs.url") -or (test-path "\\$computer\c$\documents and settings\all users\desktop\Kodak Direct View Pacs.url") ){"1"}else{"0"}
                    )
                }
            }
            else{
                $object = [pscustomobject] @{
                    Computer=$computer;
                    Available=0;
                    Kodak="NA"
                }
            }

            $object

    .EXAMPLE
        Invoke-Parallel -scriptfile C:\public\Test-ForPacs.ps1 -inputobject $(get-content C:\pcs.txt) -runspaceTimeout 10 -throttle 10

            Pulls list of PCs from C:\pcs.txt,
            Runs Test-ForPacs against each
            If any query takes longer than 10 seconds, it is disposed
            Only run 10 threads at a time

    .EXAMPLE
        Invoke-Parallel -scriptfile C:\public\Test-ForPacs.ps1 -inputobject c-is-ts-91, c-is-ts-95

            Runs against c-is-ts-91, c-is-ts-95 (-computername)
            Runs Test-ForPacs against each

    .EXAMPLE
        $stuff = [pscustomobject] @{
            ContentFile = "windows\system32\drivers\etc\hosts"
            Logfile = "C:\temp\log.txt"
        }

        $computers | Invoke-Parallel -parameter $stuff {
            $contentFile = join-path "\\$_\c$" $parameter.contentfile
            Get-Content $contentFile |
                set-content $parameter.logfile
        }

        This example uses the parameter argument.  This parameter is a single object.  To pass multiple items into the script block, we create a custom object (using a PowerShell v3 language) with properties we want to pass in.

        Inside the script block, $parameter is used to reference this parameter object.  This example sets a content file, gets content from that file, and sets it to a predefined log file.

    .EXAMPLE
        $test = 5
        1..2 | Invoke-Parallel -ImportVariables {$_ * $test}

        Add variables from the current session to the session state.  Without -ImportVariables $Test would not be accessible

    .EXAMPLE
        $test = 5
        1..2 | Invoke-Parallel {$_ * $Using:test}

        Reference a variable from the current session with the $Using: syntax.  Requires PowerShell 3 or later. Note that -ImportVariables parameter is no longer necessary.

    .FUNCTIONALITY
        PowerShell Language

    .NOTES
        Credit to Boe Prox for the base runspace code and $Using implementation
            
            http://gallery.technet.microsoft.com/scriptcenter/Speedy-Network-Information-5b1406fb#content
            https://github.com/proxb/PoshRSJob/

        Credit to T Bryce Yehl for the Quiet and NoCloseOnTimeout implementations

        Credit to Sergei Vorobev for the many ideas and contributions that have improved functionality, reliability, and ease of use

    .LINK
        https://github.com/RamblingCookieMonster/Invoke-Parallel
    #>
    [cmdletbinding(DefaultParameterSetName = 'ScriptBlock')]
    Param (
        [Parameter(Mandatory = $false, position = 0, ParameterSetName = 'ScriptBlock')]
        [System.Management.Automation.ScriptBlock]$ScriptBlock,

        [Parameter(Mandatory = $false, ParameterSetName = 'ScriptFile')]
        [ValidateScript( {Test-Path $_ -pathtype leaf})]
        $ScriptFile,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Alias('CN', '__Server', 'IPAddress', 'Server', 'ComputerName')]
        [PSObject]$InputObject,

        [PSObject]$Parameter,

        [switch]$ImportVariables,
        [switch]$ImportModules,
        [switch]$ImportFunctions,

        [int]$Throttle = 20,
        [int]$SleepTimer = 200,
        [int]$RunspaceTimeout = 0,
        [switch]$NoCloseOnTimeout = $false,
        [int]$MaxQueue,

        [validatescript( {Test-Path (Split-Path $_ -parent)})]
        [switch] $AppendLog = $false,
        [string]$LogFile,

        [switch] $Quiet = $false
    )
    begin {
        #No max queue specified?  Estimate one.
        #We use the script scope to resolve an odd PowerShell 2 issue where MaxQueue isn't seen later in the function
        if ( -not $PSBoundParameters.ContainsKey('MaxQueue') ) {
            if ($RunspaceTimeout -ne 0) { $script:MaxQueue = $Throttle }
            else { $script:MaxQueue = $Throttle * 3 }
        }
        else {
            $script:MaxQueue = $MaxQueue
        }
        Write-Verbose "Throttle: '$throttle' SleepTimer '$sleepTimer' runSpaceTimeout '$runspaceTimeout' maxQueue '$maxQueue' logFile '$logFile'"

        #If they want to import variables or modules, create a clean runspace, get loaded items, use those to exclude items
        if ($ImportVariables -or $ImportModules -or $ImportFunctions) {
            $StandardUserEnv = [powershell]::Create().addscript( {

                    #Get modules, snapins, functions in this clean runspace
                    $Modules = Get-Module | Select-Object -ExpandProperty Name
                    $Snapins = Get-PSSnapin | Select-Object -ExpandProperty Name
                    $Functions = Get-ChildItem function:\ | Select-Object -ExpandProperty Name

                    #Get variables in this clean runspace
                    #Called last to get vars like $? into session
                    $Variables = Get-Variable | Select-Object -ExpandProperty Name

                    #Return a hashtable where we can access each.
                    @{
                        Variables = $Variables
                        Modules   = $Modules
                        Snapins   = $Snapins
                        Functions = $Functions
                    }
                }).invoke()[0]

            if ($ImportVariables) {
                #Exclude common parameters, bound parameters, and automatic variables
                Function _temp {[cmdletbinding(SupportsShouldProcess = $True)] param() }
                $VariablesToExclude = @( (Get-Command _temp | Select-Object -ExpandProperty parameters).Keys + $PSBoundParameters.Keys + $StandardUserEnv.Variables )
                Write-Verbose "Excluding variables $( ($VariablesToExclude | Sort-Object ) -join ", ")"

                # we don't use 'Get-Variable -Exclude', because it uses regexps.
                # One of the veriables that we pass is '$?'.
                # There could be other variables with such problems.
                # Scope 2 required if we move to a real module
                $UserVariables = @( Get-Variable | Where-Object { -not ($VariablesToExclude -contains $_.Name) } )
                Write-Verbose "Found variables to import: $( ($UserVariables | Select-Object -expandproperty Name | Sort-Object ) -join ", " | Out-String).`n"
            }
            if ($ImportModules) {
                $UserModules = @( Get-Module | Where-Object {$StandardUserEnv.Modules -notcontains $_.Name -and (Test-Path $_.Path -ErrorAction SilentlyContinue)} | Select-Object -ExpandProperty Path )
                $UserSnapins = @( Get-PSSnapin | Select-Object -ExpandProperty Name | Where-Object {$StandardUserEnv.Snapins -notcontains $_ } )
            }
            if ($ImportFunctions) {
                $UserFunctions = @( Get-ChildItem function:\ | Where-Object { $StandardUserEnv.Functions -notcontains $_.Name } )
            }
        }

        #region functions
        Function Get-RunspaceData {
            [cmdletbinding()]
            param( [switch]$Wait )
            #loop through runspaces
            #if $wait is specified, keep looping until all complete
            Do {
                #set more to false for tracking completion
                $more = $false

                #Progress bar if we have inputobject count (bound parameter)
                if (-not $Quiet) {
                    Write-Progress  -Activity "Running Query" -Status "Starting threads"`
                        -CurrentOperation "$startedCount threads defined - $totalCount input objects - $script:completedCount input objects processed"`
                        -PercentComplete $( Try { $script:completedCount / $totalCount * 100 } Catch {0} )
                }

                #run through each runspace.
                Foreach ($runspace in $runspaces) {

                    #get the duration - inaccurate
                    $currentdate = Get-Date
                    $runtime = $currentdate - $runspace.startTime
                    $runMin = [math]::Round( $runtime.totalminutes , 2 )

                    #set up log object
                    $log = "" | Select-Object Date, Action, Runtime, Status, Details
                    $log.Action = "Removing:'$($runspace.object)'"
                    $log.Date = $currentdate
                    $log.Runtime = "$runMin minutes"

                    #If runspace completed, end invoke, dispose, recycle, counter++
                    If ($runspace.Runspace.isCompleted) {

                        $script:completedCount++

                        #check if there were errors
                        if ($runspace.powershell.Streams.Error.Count -gt 0) {
                            #set the logging info and move the file to completed
                            $log.status = "CompletedWithErrors"
                            Write-Verbose ($log | ConvertTo-Csv -Delimiter ";" -NoTypeInformation)[1]
                            foreach ($ErrorRecord in $runspace.powershell.Streams.Error) {
                                Write-Error -ErrorRecord $ErrorRecord
                            }
                        }
                        else {
                            #add logging details and cleanup
                            $log.status = "Completed"
                            Write-Verbose ($log | ConvertTo-Csv -Delimiter ";" -NoTypeInformation)[1]
                        }

                        #everything is logged, clean up the runspace
                        $runspace.powershell.EndInvoke($runspace.Runspace)
                        $runspace.powershell.dispose()
                        $runspace.Runspace = $null
                        $runspace.powershell = $null
                    }
                    #If runtime exceeds max, dispose the runspace
                    ElseIf ( $runspaceTimeout -ne 0 -and $runtime.totalseconds -gt $runspaceTimeout) {
                        $script:completedCount++
                        $timedOutTasks = $true

                        #add logging details and cleanup
                        $log.status = "TimedOut"
                        Write-Verbose ($log | ConvertTo-Csv -Delimiter ";" -NoTypeInformation)[1]
                        Write-Error "Runspace timed out at $($runtime.totalseconds) seconds for the object:`n$($runspace.object | out-string)"

                        #Depending on how it hangs, we could still get stuck here as dispose calls a synchronous method on the powershell instance
                        if (!$noCloseOnTimeout) { $runspace.powershell.dispose() }
                        $runspace.Runspace = $null
                        $runspace.powershell = $null
                        $completedCount++
                    }

                    #If runspace isn't null set more to true
                    ElseIf ($runspace.Runspace -ne $null ) {
                        $log = $null
                        $more = $true
                    }

                    #log the results if a log file was indicated
                    if ($logFile -and $log) {
                        ($log | ConvertTo-Csv -Delimiter ";" -NoTypeInformation)[1] | out-file $LogFile -append
                    }
                }

                #Clean out unused runspace jobs
                $temphash = $runspaces.clone()
                $temphash | Where-Object { $_.runspace -eq $Null } | ForEach-Object {
                    $Runspaces.remove($_)
                }

                #sleep for a bit if we will loop again
                if ($PSBoundParameters['Wait']) { Start-Sleep -milliseconds $SleepTimer }

                #Loop again only if -wait parameter and there are more runspaces to process
            } while ($more -and $PSBoundParameters['Wait'])

            #End of runspace function
        }
        #endregion functions

        #region Init

        if ($PSCmdlet.ParameterSetName -eq 'ScriptFile') {
            $ScriptBlock = [scriptblock]::Create( $(Get-Content $ScriptFile | out-string) )
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') {
            #Start building parameter names for the param block
            [string[]]$ParamsToAdd = '$_'
            if ( $PSBoundParameters.ContainsKey('Parameter') ) {
                $ParamsToAdd += '$Parameter'
            }

            $UsingVariableData = $Null

            # This code enables $Using support through the AST.
            # This is entirely from  Boe Prox, and his https://github.com/proxb/PoshRSJob module; all credit to Boe!

            if ($PSVersionTable.PSVersion.Major -gt 2) {
                #Extract using references
                $UsingVariables = $ScriptBlock.ast.FindAll( {$args[0] -is [System.Management.Automation.Language.UsingExpressionAst]}, $True)

                If ($UsingVariables) {
                    $List = New-Object 'System.Collections.Generic.List`1[System.Management.Automation.Language.VariableExpressionAst]'
                    ForEach ($Ast in $UsingVariables) {
                        [void]$list.Add($Ast.SubExpression)
                    }

                    $UsingVar = $UsingVariables | Group-Object -Property SubExpression | ForEach-Object {$_.Group | Select-Object -First 1}

                    #Extract the name, value, and create replacements for each
                    $UsingVariableData = ForEach ($Var in $UsingVar) {
                        try {
                            $Value = Get-Variable -Name $Var.SubExpression.VariablePath.UserPath -ErrorAction Stop
                            [pscustomobject]@{
                                Name       = $Var.SubExpression.Extent.Text
                                Value      = $Value.Value
                                NewName    = ('$__using_{0}' -f $Var.SubExpression.VariablePath.UserPath)
                                NewVarName = ('__using_{0}' -f $Var.SubExpression.VariablePath.UserPath)
                            }
                        }
                        catch {
                            Write-Error "$($Var.SubExpression.Extent.Text) is not a valid Using: variable!"
                        }
                    }
                    $ParamsToAdd += $UsingVariableData | Select-Object -ExpandProperty NewName -Unique

                    $NewParams = $UsingVariableData.NewName -join ', '
                    $Tuple = [Tuple]::Create($list, $NewParams)
                    $bindingFlags = [Reflection.BindingFlags]"Default,NonPublic,Instance"
                    $GetWithInputHandlingForInvokeCommandImpl = ($ScriptBlock.ast.gettype().GetMethod('GetWithInputHandlingForInvokeCommandImpl', $bindingFlags))

                    $StringScriptBlock = $GetWithInputHandlingForInvokeCommandImpl.Invoke($ScriptBlock.ast, @($Tuple))

                    $ScriptBlock = [scriptblock]::Create($StringScriptBlock)

                    Write-Verbose $StringScriptBlock
                }
            }

            $ScriptBlock = $ExecutionContext.InvokeCommand.NewScriptBlock("param($($ParamsToAdd -Join ", "))`r`n" + $Scriptblock.ToString())
        }
        else {
            Throw "Must provide ScriptBlock or ScriptFile"; Break
        }

        Write-Debug "`$ScriptBlock: $($ScriptBlock | Out-String)"
        Write-Verbose "Creating runspace pool and session states"

        #If specified, add variables and modules/snapins to session state
        $sessionstate = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
        if ($ImportVariables -and $UserVariables.count -gt 0) {
            foreach ($Variable in $UserVariables) {
                $sessionstate.Variables.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList $Variable.Name, $Variable.Value, $null) )
            }
        }
        if ($ImportModules) {
            if ($UserModules.count -gt 0) {
                foreach ($ModulePath in $UserModules) {
                    $sessionstate.ImportPSModule($ModulePath)
                }
            }
            if ($UserSnapins.count -gt 0) {
                foreach ($PSSnapin in $UserSnapins) {
                    [void]$sessionstate.ImportPSSnapIn($PSSnapin, [ref]$null)
                }
            }
        }
        if ($ImportFunctions -and $UserFunctions.count -gt 0) {
            foreach ($FunctionDef in $UserFunctions) {
                $sessionstate.Commands.Add((New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $FunctionDef.Name, $FunctionDef.ScriptBlock))
            }
        }

        #Create runspace pool
        $runspacepool = [runspacefactory]::CreateRunspacePool(1, $Throttle, $sessionstate, $Host)
        $runspacepool.Open()

        Write-Verbose "Creating empty collection to hold runspace jobs"
        $Script:runspaces = New-Object System.Collections.ArrayList

        #If inputObject is bound get a total count and set bound to true
        $bound = $PSBoundParameters.keys -contains "InputObject"
        if (-not $bound) {
            [System.Collections.ArrayList]$allObjects = @()
        }

        #Set up log file if specified
        if ( $LogFile -and (-not (Test-Path $LogFile) -or $AppendLog -eq $false)) {
            New-Item -ItemType file -Path $logFile -Force | Out-Null
            ("" | Select-Object -Property Date, Action, Runtime, Status, Details | ConvertTo-Csv -NoTypeInformation -Delimiter ";")[0] | Out-File $LogFile
        }

        #write initial log entry
        $log = "" | Select-Object -Property Date, Action, Runtime, Status, Details
        $log.Date = Get-Date
        $log.Action = "Batch processing started"
        $log.Runtime = $null
        $log.Status = "Started"
        $log.Details = $null
        if ($logFile) {
            ($log | convertto-csv -Delimiter ";" -NoTypeInformation)[1] | Out-File $LogFile -Append
        }
        $timedOutTasks = $false
        #endregion INIT
    }
    process {
        #add piped objects to all objects or set all objects to bound input object parameter
        if ($bound) {
            $allObjects = $InputObject
        }
        else {
            [void]$allObjects.add( $InputObject )
        }
    }
    end {
        #Use Try/Finally to catch Ctrl+C and clean up.
        try {
            #counts for progress
            $totalCount = $allObjects.count
            $script:completedCount = 0
            $startedCount = 0
            foreach ($object in $allObjects) {
                #region add scripts to runspace pool
                #Create the powershell instance, set verbose if needed, supply the scriptblock and parameters
                $powershell = [powershell]::Create()

                if ($VerbosePreference -eq 'Continue') {
                    [void]$PowerShell.AddScript( {$VerbosePreference = 'Continue'})
                }

                [void]$PowerShell.AddScript($ScriptBlock).AddArgument($object)

                if ($parameter) {
                    [void]$PowerShell.AddArgument($parameter)
                }

                # $Using support from Boe Prox
                if ($UsingVariableData) {
                    Foreach ($UsingVariable in $UsingVariableData) {
                        Write-Verbose "Adding $($UsingVariable.Name) with value: $($UsingVariable.Value)"
                        [void]$PowerShell.AddArgument($UsingVariable.Value)
                    }
                }

                #Add the runspace into the powershell instance
                $powershell.RunspacePool = $runspacepool

                #Create a temporary collection for each runspace
                $temp = "" | Select-Object PowerShell, StartTime, object, Runspace
                $temp.PowerShell = $powershell
                $temp.StartTime = Get-Date
                $temp.object = $object

                #Save the handle output when calling BeginInvoke() that will be used later to end the runspace
                $temp.Runspace = $powershell.BeginInvoke()
                $startedCount++

                #Add the temp tracking info to $runspaces collection
                Write-Verbose ( "Adding {0} to collection at {1}" -f $temp.object, $temp.starttime.tostring() )
                $runspaces.Add($temp) | Out-Null

                #loop through existing runspaces one time
                Get-RunspaceData

                #If we have more running than max queue (used to control timeout accuracy)
                #Script scope resolves odd PowerShell 2 issue
                $firstRun = $true
                while ($runspaces.count -ge $Script:MaxQueue) {
                    #give verbose output
                    if ($firstRun) {
                        Write-Verbose "$($runspaces.count) items running - exceeded $Script:MaxQueue limit."
                    }
                    $firstRun = $false

                    #run get-runspace data and sleep for a short while
                    Get-RunspaceData
                    Start-Sleep -Milliseconds $sleepTimer
                }
                #endregion add scripts to runspace pool
            }
            Write-Verbose ( "Finish processing the remaining runspace jobs: {0}" -f ( @($runspaces | Where-Object {$_.Runspace -ne $Null}).Count) )

            Get-RunspaceData -wait
            if (-not $quiet) {
                Write-Progress -Activity "Running Query" -Status "Starting threads" -Completed
            }
        }
        finally {
            #Close the runspace pool, unless we specified no close on timeout and something timed out
            if ( ($timedOutTasks -eq $false) -or ( ($timedOutTasks -eq $true) -and ($noCloseOnTimeout -eq $false) ) ) {
                Write-Verbose "Closing the runspace pool"
                $runspacepool.close()
            }
            #collect garbage
            [gc]::Collect()
        }
    }
}

$GetMeltdownStatusInformation = {
    # Based on https://www.powershellgallery.com/packages/SpeculationControl/1.0.2
    function Get-SpeculationControlSettings {
        <# 
 
  .SYNOPSIS 
  This function queries the speculation control settings for the system. 
 
  .DESCRIPTION 
  This function queries the speculation control settings for the system. 
 
  Version 1.3. 
   
  #>

        [CmdletBinding()]
        param (

        )
  
        process {

            $NtQSIDefinition = @' 
    [DllImport("ntdll.dll")] 
    public static extern int NtQuerySystemInformation(uint systemInformationClass, IntPtr systemInformation, uint systemInformationLength, IntPtr returnLength); 
'@
    
            $ntdll = Add-Type -MemberDefinition $NtQSIDefinition -Name 'ntdll' -Namespace 'Win32' -PassThru


            [System.IntPtr]$systemInformationPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(4)
            [System.IntPtr]$returnLengthPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(4)

            $object = New-Object -TypeName PSObject

            try {
    
                #
                # Query branch target injection information.
                #

                #Write-Host "Speculation control settings for CVE-2017-5715 [branch target injection]" -ForegroundColor Cyan
                #Write-Host

                $btiHardwarePresent = $false
                $btiWindowsSupportPresent = $false
                $btiWindowsSupportEnabled = $false
                $btiDisabledBySystemPolicy = $false
                $btiDisabledByNoHardwareSupport = $false
    
                [System.UInt32]$systemInformationClass = 201
                [System.UInt32]$systemInformationLength = 4

                $retval = $ntdll::NtQuerySystemInformation($systemInformationClass, $systemInformationPtr, $systemInformationLength, $returnLengthPtr)

                if ($retval -eq 0xc0000003 -or $retval -eq 0xc0000002) {
                    # fallthrough
                }
                elseif ($retval -ne 0) {
                    throw (("Querying branch target injection information failed with error {0:X8}" -f $retval))
                }
                else {
    
                    [System.UInt32]$scfBpbEnabled = 0x01
                    [System.UInt32]$scfBpbDisabledSystemPolicy = 0x02
                    [System.UInt32]$scfBpbDisabledNoHardwareSupport = 0x04
                    [System.UInt32]$scfHwReg1Enumerated = 0x08
                    [System.UInt32]$scfHwReg2Enumerated = 0x10
                    [System.UInt32]$scfHwMode1Present = 0x20
                    [System.UInt32]$scfHwMode2Present = 0x40
                    [System.UInt32]$scfSmepPresent = 0x80

                    [System.UInt32]$flags = [System.UInt32][System.Runtime.InteropServices.Marshal]::ReadInt32($systemInformationPtr)

                    $btiHardwarePresent = ((($flags -band $scfHwReg1Enumerated) -ne 0) -or (($flags -band $scfHwReg2Enumerated)))
                    $btiWindowsSupportPresent = $true
                    $btiWindowsSupportEnabled = (($flags -band $scfBpbEnabled) -ne 0)

                    if ($btiWindowsSupportEnabled -eq $false) {
                        $btiDisabledBySystemPolicy = (($flags -band $scfBpbDisabledSystemPolicy) -ne 0)
                        $btiDisabledByNoHardwareSupport = (($flags -band $scfBpbDisabledNoHardwareSupport) -ne 0)
                    }

                    if ($PSBoundParameters['Verbose']) {
                        #Write-Host "BpbEnabled :" (($flags -band $scfBpbEnabled) -ne 0)
                        #Write-Host "BpbDisabledSystemPolicy :" (($flags -band $scfBpbDisabledSystemPolicy) -ne 0)
                        #Write-Host "BpbDisabledNoHardwareSupport :" (($flags -band $scfBpbDisabledNoHardwareSupport) -ne 0)
                        #Write-Host "HwReg1Enumerated :" (($flags -band $scfHwReg1Enumerated) -ne 0)
                        #Write-Host "HwReg2Enumerated :" (($flags -band $scfHwReg2Enumerated) -ne 0)
                        #Write-Host "HwMode1Present :" (($flags -band $scfHwMode1Present) -ne 0)
                        #Write-Host "HwMode2Present :" (($flags -band $scfHwMode2Present) -ne 0)
                        #Write-Host "SmepPresent :" (($flags -band $scfSmepPresent) -ne 0)
                    }
                }

                #Write-Host "Hardware support for branch target injection mitigation is present:"($btiHardwarePresent) -ForegroundColor $(If ($btiHardwarePresent) { [System.ConsoleColor]::Green } Else { [System.ConsoleColor]::Red })
                #Write-Host "Windows OS support for branch target injection mitigation is present:"($btiWindowsSupportPresent) -ForegroundColor $(If ($btiWindowsSupportPresent) { [System.ConsoleColor]::Green } Else { [System.ConsoleColor]::Red })
                #Write-Host "Windows OS support for branch target injection mitigation is enabled:"($btiWindowsSupportEnabled) -ForegroundColor $(If ($btiWindowsSupportEnabled) { [System.ConsoleColor]::Green } Else { [System.ConsoleColor]::Red })
  
                if ($btiWindowsSupportPresent -eq $true -and $btiWindowsSupportEnabled -eq $false) {
                    #Write-Host -ForegroundColor Red "Windows OS support for branch target injection mitigation is disabled by system policy:"($btiDisabledBySystemPolicy)
                    #Write-Host -ForegroundColor Red "Windows OS support for branch target injection mitigation is disabled by absence of hardware support:"($btiDisabledByNoHardwareSupport)
                }
        
                $object | Add-Member -MemberType NoteProperty -Name BTIHardwarePresent -Value $btiHardwarePresent
                $object | Add-Member -MemberType NoteProperty -Name BTIWindowsSupportPresent -Value $btiWindowsSupportPresent
                $object | Add-Member -MemberType NoteProperty -Name BTIWindowsSupportEnabled -Value $btiWindowsSupportEnabled
                $object | Add-Member -MemberType NoteProperty -Name BTIDisabledBySystemPolicy -Value $btiDisabledBySystemPolicy
                $object | Add-Member -MemberType NoteProperty -Name BTIDisabledByNoHardwareSupport -Value $btiDisabledByNoHardwareSupport

                #
                # Query kernel VA shadow information.
                #

                #Write-Host
                #Write-Host "Speculation control settings for CVE-2017-5754 [rogue data cache load]" -ForegroundColor Cyan
                #Write-Host    

                $kvaShadowRequired = $true
                $kvaShadowPresent = $false
                $kvaShadowEnabled = $false
                $kvaShadowPcidEnabled = $false

                $cpu = Get-WmiObject -Class Win32_Processor | Select-Object -First 1 #Fix for the case of multiple objects returned

                if ($cpu.Manufacturer -eq "AuthenticAMD") {
                    $kvaShadowRequired = $false
                }
                elseif ($cpu.Manufacturer -eq "GenuineIntel") {
                    $regex = [regex]'Family (\d+) Model (\d+) Stepping (\d+)'
                    $result = $regex.Match($cpu.Description)
            
                    if ($result.Success) {
                        $family = [System.UInt32]$result.Groups[1].Value
                        $model = [System.UInt32]$result.Groups[2].Value
                        $stepping = [System.UInt32]$result.Groups[3].Value
                
                        if (($family -eq 0x6) -and 
                            (($model -eq 0x1c) -or
                                ($model -eq 0x26) -or
                                ($model -eq 0x27) -or
                                ($model -eq 0x36) -or
                                ($model -eq 0x35))) {

                            $kvaShadowRequired = $false
                        }
                    }
                }
                else {
                    throw ("Unsupported processor manufacturer: {0}" -f $cpu.Manufacturer)
                }

                [System.UInt32]$systemInformationClass = 196
                [System.UInt32]$systemInformationLength = 4

                $retval = $ntdll::NtQuerySystemInformation($systemInformationClass, $systemInformationPtr, $systemInformationLength, $returnLengthPtr)

                if ($retval -eq 0xc0000003 -or $retval -eq 0xc0000002) {
                }
                elseif ($retval -ne 0) {
                    throw (("Querying kernel VA shadow information failed with error {0:X8}" -f $retval))
                }
                else {
    
                    [System.UInt32]$kvaShadowEnabledFlag = 0x01
                    [System.UInt32]$kvaShadowUserGlobalFlag = 0x02
                    [System.UInt32]$kvaShadowPcidFlag = 0x04
                    [System.UInt32]$kvaShadowInvpcidFlag = 0x08

                    [System.UInt32]$flags = [System.UInt32][System.Runtime.InteropServices.Marshal]::ReadInt32($systemInformationPtr)

                    $kvaShadowPresent = $true
                    $kvaShadowEnabled = (($flags -band $kvaShadowEnabledFlag) -ne 0)
                    $kvaShadowPcidEnabled = ((($flags -band $kvaShadowPcidFlag) -ne 0) -and (($flags -band $kvaShadowInvpcidFlag) -ne 0))

                    if ($PSBoundParameters['Verbose']) {
                        #Write-Host "KvaShadowEnabled :" (($flags -band $kvaShadowEnabledFlag) -ne 0)
                        #Write-Host "KvaShadowUserGlobal :" (($flags -band $kvaShadowUserGlobalFlag) -ne 0)
                        #Write-Host "KvaShadowPcid :" (($flags -band $kvaShadowPcidFlag) -ne 0)
                        #Write-Host "KvaShadowInvpcid :" (($flags -band $kvaShadowInvpcidFlag) -ne 0)
                    }
                }
        
                #Write-Host "Hardware requires kernel VA shadowing:"$kvaShadowRequired

                if ($kvaShadowRequired) {

                    #Write-Host "Windows OS support for kernel VA shadow is present:"$kvaShadowPresent -ForegroundColor $(If ($kvaShadowPresent) { [System.ConsoleColor]::Green } Else { [System.ConsoleColor]::Red })
                    #Write-Host "Windows OS support for kernel VA shadow is enabled:"$kvaShadowEnabled -ForegroundColor $(If ($kvaShadowEnabled) { [System.ConsoleColor]::Green } Else { [System.ConsoleColor]::Red })

                    if ($kvaShadowEnabled) {
                        #Write-Host "Windows OS support for PCID performance optimization is enabled: $kvaShadowPcidEnabled [not required for security]" -ForegroundColor $(If ($kvaShadowPcidEnabled) { [System.ConsoleColor]::Green } Else { [System.ConsoleColor]::Blue })
                    }
                }

        
                $object | Add-Member -MemberType NoteProperty -Name KVAShadowRequired -Value $kvaShadowRequired
                $object | Add-Member -MemberType NoteProperty -Name KVAShadowWindowsSupportPresent -Value $kvaShadowPresent
                $object | Add-Member -MemberType NoteProperty -Name KVAShadowWindowsSupportEnabled -Value $kvaShadowEnabled
                $object | Add-Member -MemberType NoteProperty -Name KVAShadowPcidEnabled -Value $kvaShadowPcidEnabled

                #
                # Provide guidance as appropriate.
                #

                $actions = @()
        
                if ($btiHardwarePresent -eq $false) {
                    $actions += "Install BIOS/firmware update provided by your device OEM that enables hardware support for the branch target injection mitigation."
                }

                if ($btiWindowsSupportPresent -eq $false -or $kvaShadowPresent -eq $false) {
                    $actions += "Install the latest available updates for Windows with support for speculation control mitigations."
                }

                if (($btiHardwarePresent -eq $true -and $btiWindowsSupportEnabled -eq $false) -or ($kvaShadowRequired -eq $true -and $kvaShadowEnabled -eq $false)) {
                    $guidanceUri = ""
                    $guidanceType = ""

            
                    $os = Get-WmiObject Win32_OperatingSystem

                    if ($os.ProductType -eq 1) {
                        # Workstation
                        $guidanceUri = "https://support.microsoft.com/help/4073119"
                        $guidanceType = "Client"
                    }
                    else {
                        # Server/DC
                        $guidanceUri = "https://support.microsoft.com/help/4072698"
                        $guidanceType = "Server"
                    }

                    $actions += "Follow the guidance for enabling Windows $guidanceType support for speculation control mitigations described in $guidanceUri"
                }

                if ($actions.Length -gt 0) {

                    #Write-Host
                    #Write-Host "Suggested actions" -ForegroundColor Cyan
                    #Write-Host 

                    foreach ($action in $actions) {
                        #Write-Host " *" $action
                    }
                }


                return $object

            }
            finally {
                if ($systemInformationPtr -ne [System.IntPtr]::Zero) {
                    [System.Runtime.InteropServices.Marshal]::FreeHGlobal($systemInformationPtr)
                }
 
                if ($returnLengthPtr -ne [System.IntPtr]::Zero) {
                    [System.Runtime.InteropServices.Marshal]::FreeHGlobal($returnLengthPtr)
                }
            }    
        }
    }    
    function Get-SystemInformation {
        $ComputerName = $env:COMPUTERNAME
        $Win32_ComputerSystem = Get-WmiObject -Class Win32_ComputerSystem
        $Win32_OperatingSystem = Get-WmiObject -Class Win32_OperatingSystem
        $ComputerManufacturer = $Win32_ComputerSystem.Manufacturer
        $ComputerModel = $Win32_ComputerSystem.Model
        $ProductType = $Win32_OperatingSystem.ProductType
        $BIOS = (Get-WmiObject -Class Win32_BIOS).Name
        $Processor = (Get-WmiObject -Class Win32_Processor).Name
        $OperatingSystem = $Win32_OperatingSystem.Caption
        $OSReleaseId = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction SilentlyContinue).ReleaseId
        $LastReboot = [Management.ManagementDateTimeConverter]::ToDateTime($Win32_OperatingSystem.LastBootUptime)
        $Uptime = ((Get-Date) - $LastReboot).ToString()
        $Hotfixes = Get-WmiObject -Class Win32_QuickFixEngineering | 
            Select-Object HotFixId, Description, InstalledOn, @{
            Name       = 'ComputerName'; 
            Expression = {$env:COMPUTERNAME}
        } | Sort-Object HotFixId
        $ExecutionDate = Get-Date -Format d

        $vmms = Get-Service -Name vmms -ErrorAction SilentlyContinue
        if ($vmms.Status -eq 'Running') {
            $isHyperV = $true
        }
        else {
            $isHyperV = $false
        }

        $TerminalServerMode = (Get-WmiObject -Namespace root\CIMV2/TerminalServices -Class Win32_TerminalServiceSetting).TerminalServerMode
        if ($TerminalServerMode -eq 1) {
            $isTerminalServer = $true
        }
        else {
            $isTerminalServer = $false
        }

        # Test for Docker
        if ($env:Path -match 'docker') {
            $isDocker = $true
        }
        else {
            $isDocker = $false
        }

        # Test for Chrome 
        # WMI Class Win32_Product does not show Chrome for me.
        # Win32_InstalledWin32Program requies administrative privileges and Windows 7
        $isChrome = Test-Path -Path 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'  

        # Test for Edge
        if ($OSReleaseId) {
            # Is Windows 10
            if (Get-AppxPackage -Name Microsoft.MicrosoftEdge) {
                $isEdge = $true
            }
            else {
                $isEdge = $false
            }
        }
        else {
            $isEdge = $false
        }

        # Test for IE
        $isIE = Test-Path -Path 'C:\Program Files\Internet Explorer\iexplore.exe'

        # Test for Firefox
        $isFirefox = (Test-Path -Path 'C:\Program Files\Mozilla Firefox\firefox.exe') -or
        (Test-Path -Path 'C:\Program Files (x86)\Mozilla Firefox\firefox.exe')

        <#
        Customers need to enable mitigations to help protect against speculative execution side-channel vulnerabilities.

        Enabling these mitigations may affect performance. The actual performance impact will depend on multiple factors such as the specific chipset in your physical host and the workloads that are running. Microsoft recommends customers assess the performance impact for their environment and make the necessary adjustments if needed.

        Your server is at increased risk if your server falls into one of the following categories:

        Hyper-V hosts
        Remote Desktop Services Hosts (RDSH)
        For physical hosts or virtual machines that are running untrusted code such as containers or untrusted extensions for database, untrusted web content or workloads that run code that is provided from external sources.
        #>
        #if ($ProductType -ne 1) {
            # Product Type = Workstation
            $FeatureSettingsOverride = (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management' -ErrorAction SilentlyContinue).FeatureSettingsOverride # must be 0
            $FeatureSettingsOverrideMask = (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management' -ErrorAction SilentlyContinue).FeatureSettingsOverrideMask # must be 3
            if (($FeatureSettingsOverride -eq 0) -and ($FeatureSettingsOverrideMask -eq 3)) {
                $OSMitigationRegKeySet = $true
            }
            else {
                $OSMitigationRegKeySet = $false
            }
        #}

        # https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/CVE-2017-5715-and-hyper-v-vms
        if ($isHyperV) {
            $MinVmVersionForCpuBasedMitigations = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization' -ErrorAction SilentlyContinue).MinVmVersionForCpuBasedMitigations
            if (-not $MinVmVersionForCpuBasedMitigations) {
                if ($OSReleaseId) {
                    $MinVmVersionForCpuBasedMitigations = '8.0'
                }
                else {
                    $MinVmVersionForCpuBasedMitigations = $false
                }
            }
        }

        <#
        Customers without Anti-Virus
        Microsoft recommends all customers protect their devices by running a supported anti-virus program. Customers can also take advantage of built-in anti-virus protection, Windows Defender for Windows 10 devices or Microsoft Security Essentials for Windows 7 devices. These solutions are compatible in cases where customers can’t install or run anti-virus software. Microsoft recommends manually setting the registry key in the following section to receive the January 2018 security updates.
        #>
        $AVRegKeyValue = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\QualityCompat' -ErrorAction SilentlyContinue).'cadca5fe-87d3-4b96-b7fb-a231484277cc' # must be 0
        if ($AVRegKeyValue -eq 0) {
            $AVCompatibility = $true
        }
        else {
            $AVCompatibility = $false
        }

        $output = New-Object -TypeName PSCustomObject
        $output | Add-Member -MemberType NoteProperty -Name ComputerName -Value $ComputerName
        $output | Add-Member -MemberType NoteProperty -Name Manufacturer -Value $ComputerManufacturer
        $output | Add-Member -MemberType NoteProperty -Name Model -Value $ComputerModel
        $output | Add-Member -MemberType NoteProperty -Name BIOS -Value $BIOS
        $output | Add-Member -MemberType NoteProperty -Name CPU -Value $Processor
        $output | Add-Member -MemberType NoteProperty -Name OperatingSystem -Value $OperatingSystem
        $output | Add-Member -MemberType NoteProperty -Name ProductType -Value $ProductType
        $output | Add-Member -MemberType NoteProperty -Name OSReleaseId -Value $OSReleaseId
        $output | Add-Member -MemberType NoteProperty -Name isHyperV -Value $isHyperV
        $output | Add-Member -MemberType NoteProperty -Name isTerminalServer -Value $isTerminalServer
        $output | Add-Member -MemberType NoteProperty -Name isDocker -Value $isDocker
        $output | Add-Member -MemberType NoteProperty -Name isEdge -Value $isEdge
        $output | Add-Member -MemberType NoteProperty -Name isIE -Value $isIE
        $output | Add-Member -MemberType NoteProperty -Name isChrome -Value $isChrome
        $output | Add-Member -MemberType NoteProperty -Name isFirefox -Value $isFirefox        
        $output | Add-Member -MemberType NoteProperty -Name OSMitigationRegKeySet -Value $OSMitigationRegKeySet
        $output | Add-Member -MemberType NoteProperty -Name AVCompatibility -Value $AVCompatibility
        $output | Add-Member -MemberType NoteProperty -Name MinVmVersionForCpuBasedMitigations -Value $MinVmVersionForCpuBasedMitigations
        $output | Add-Member -MemberType NoteProperty -Name InstalledUpdates -Value $Hotfixes
        $output | Add-Member -MemberType NoteProperty -Name Uptime -Value $Uptime
        $output | Add-Member -MemberType NoteProperty -Name ExecutionDate -Value $ExecutionDate
        $output
    }

    # CVE-2017-5754 (Meltdown)
    function Get-CVE-2017-5754 ($SpeculationControlSettings, $SystemInformation) {
        if ($SpeculationControlSettings.KVAShadowRequired -eq $false) {
            $mitigated = $true
        }
        elseif (($SpeculationControlSettings.KVAShadowWindowsSupportPresent -eq $true) -and 
            ($SpeculationControlSettings.KVAShadowWindowsSupportEnabled -eq $true) -and
            ($SpeculationControlSettings.KVAShadowPcidEnabled -eq $true)) {
            $mitigated = $true
        }
        else {
            $mitigated = $false
        }
        $mitigated        
    }
    
    # CVE-2017-5715 (Spectre)
    function Get-CVE-2017-5715 ($SpeculationControlSettings, $SystemInformation) {
        # probably more -and then required, but better safe then sorry
        if (($SpeculationControlSettings.BTIHardwarePresent -eq $true) -and 
            ($SpeculationControlSettings.BTIWindowsSupportPresent -eq $true) -and
            ($SpeculationControlSettings.BTIWindowsSupportEnabled -eq $true)) {
            $mitigated = $true
        }
        else {
            $mitigated = $false
        }
        $mitigated
    }   

    # CVE-2017-5753 (Spectre)
    function Get-CVE-2017-5753 ($SystemInformation) {
        function IsHotfixInstalled ($ListOfRequiredKBs, $ListOfInstalledKBs) {
            <#
            .SYNOPSIS
                If any of the required KBs is installed, the function returns true
            #>
            foreach ($KB in $ListOfRequiredKBs) {
                if ($ListOfInstalledKBs -contains $KB) {
                    $installed = $true
                    break
                }
            }
            if ($installed) {
                $true
            }
            else {
                $false
            }
        }

        # Chrome
        # https://www.chromium.org/Home/chromium-security/site-isolation 
        if ($SystemInformation.isChrome) {
            $ChromeVersion = (Get-Item 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe').VersionInfo.ProductVersion -as [version]
            if ($ChromeVersion.Major -gt 63) {
                $ChromeMitigated = $true
            }
            elseif ($ChromeVersion.Major -eq 63) {
                $ChromeSitePerProcessSetting = (Get-ItemProperty -Path HKLM:\Software\Policies\Google\Chrome -ErrorAction SilentlyContinue).SitePerProcess # must be 1
                if ($ChromeSitePerProcessSetting -eq 1) {
                    $ChromeMitigated = $true
                }
                else {
                    $ChromeMitigated = $false
                }
            }
            else {
                $ChromeMitigated = $false
            }
        } 

        # Microsoft Browser (https://blogs.windows.com/msedgedev/2018/01/03/speculative-execution-mitigations-microsoft-edge-internet-explorer/)
        # From my understanding, the patch is effective as soon as the patch is installed

        # Edge
        if ($SystemInformation.isEdge) {
            #KBs from https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/ADV180002
            $EdgeUpdates = 'KB4056893', 'KB4056890', 'KB4056891', 'KB4056892', 'KB4056888'
            $Hotfixes = $SystemInformation.InstalledUpdates | Select-Object -ExpandProperty HotFixId
            $EdgeMitigated = IsHotfixInstalled $EdgeUpdates $Hotfixes
        } 

        # Internet Explorer 
        if ($SystemInformation.isIE) {
            # KBs from https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/ADV180002
            $IEUpdates = 'KB4056890', 'KB4056895', 'KB4056894', 'KB4056568', 'KB4056893', 'KB4056891', 'KB4056892'
            $Hotfixes = $SystemInformation.InstalledUpdates | Select-Object -ExpandProperty HotFixId
            $IEMitigated = IsHotfixInstalled $IEUpdates $Hotfixes
        } 

        # Firefox
        if ($SystemInformation.isFirefox) {
            # See https://blog.mozilla.org/security/2018/01/03/mitigations-landing-new-class-timing-attack/
            $Firefox = (Get-Item -Path 'C:\Program Files\Mozilla Firefox\firefox.exe', 
                'C:\Program Files (x86)\Mozilla Firefox\firefox.exe' -ErrorAction SilentlyContinue)
            $FirefoxVersion = ($Firefox.VersionInfo.ProductVersion | Sort-Object | Select-Object -First 1) -as [version]
            if ($FirefoxVersion -ge [version]'57.0.4') {
                $FirefoxMitigated = $true
            }
            else {
                $FirefoxMitigated = $false
            }
        }

        $output = New-Object -TypeName PSCustomObject
        $output | Add-Member -MemberType NoteProperty -Name EdgeMitigated -Value $EdgeMitigated
        $output | Add-Member -MemberType NoteProperty -Name IEMitigated -Value $IEMitigated
        $output | Add-Member -MemberType NoteProperty -Name ChromeMitigated -Value $ChromeMitigated
        $output | Add-Member -MemberType NoteProperty -Name FirefoxMitigated -Value $FirefoxMitigated
        $output
    }    

    $SystemInformation = Get-SystemInformation
    $SpeculationControlSettings = Get-SpeculationControlSettings -ErrorAction Continue
    $CVE20175754mitigated = Get-CVE-2017-5754 $SpeculationControlSettings $SystemInformation
    $CVE20175715mitigated = Get-CVE-2017-5715 $SpeculationControlSettings $SystemInformation
    $CVE20175753mitigated = Get-CVE-2017-5753 $SystemInformation

    $output = New-Object -TypeName PSCustomObject
    $output.PSObject.TypeNames.Insert(0, 'MeltdownSpectre.Report')
    

    $output | Add-Member -MemberType NoteProperty -Name ComputerName -Value $SystemInformation.ComputerName
    $output | Add-Member -MemberType NoteProperty -Name Manufacturer -Value $SystemInformation.Manufacturer
    $output | Add-Member -MemberType NoteProperty -Name Model -Value $SystemInformation.Model
    $output | Add-Member -MemberType NoteProperty -Name BIOS -Value $SystemInformation.BIOS
    $output | Add-Member -MemberType NoteProperty -Name CPU -Value $SystemInformation.CPU
    $output | Add-Member -MemberType NoteProperty -Name OperatingSystem -Value $SystemInformation.OperatingSystem
    $output | Add-Member -MemberType NoteProperty -Name OSReleaseId -Value $SystemInformation.OSReleaseId
    $output | Add-Member -MemberType NoteProperty -Name isHyperV -Value $SystemInformation.isHyperV
    $output | Add-Member -MemberType NoteProperty -Name isTerminalServer -Value $SystemInformation.isTerminalServer
    $output | Add-Member -MemberType NoteProperty -Name isDocker -Value $SystemInformation.isDocker
    #$output | Add-Member -MemberType NoteProperty -Name isIE -Value $SystemInformation.isIE
    #$output | Add-Member -MemberType NoteProperty -Name isEdge -Value $SystemInformation.isEdge
    #$output | Add-Member -MemberType NoteProperty -Name isChrome -Value $SystemInformation.isChrome
    #$output | Add-Member -MemberType NoteProperty -Name isFirefox -Value $SystemInformation.isFirefox
    $output | Add-Member -MemberType NoteProperty -Name 'CVE-2017-5754 mitigated' -Value $CVE20175754mitigated
    $output | Add-Member -MemberType NoteProperty -Name 'CVE-2017-5715 mitigated' -Value $CVE20175715mitigated
    $output | Add-Member -MemberType NoteProperty -Name 'CVE-2017-5753 mitigated in Edge' -Value $CVE20175753mitigated.EdgeMitigated
    $output | Add-Member -MemberType NoteProperty -Name 'CVE-2017-5753 mitigated in IE' -Value $CVE20175753mitigated.IEMitigated
    $output | Add-Member -MemberType NoteProperty -Name 'CVE-2017-5753 mitigated in Chrome' -Value $CVE20175753mitigated.ChromeMitigated
    $output | Add-Member -MemberType NoteProperty -Name 'CVE-2017-5753 mitigated in Firefox' -Value $CVE20175753mitigated.FirefoxMitigated
    $output | Add-Member -MemberType NoteProperty -Name BTIHardwarePresent -Value $SpeculationControlSettings.BTIHardwarePresent
    $output | Add-Member -MemberType NoteProperty -Name BTIWindowsSupportPresent -Value $SpeculationControlSettings.BTIWindowsSupportPresent
    $output | Add-Member -MemberType NoteProperty -Name BTIWindowsSupportEnabled -Value $SpeculationControlSettings.BTIWindowsSupportEnabled
    $output | Add-Member -MemberType NoteProperty -Name BTIDisabledBySystemPolicy -Value $SpeculationControlSettings.BTIDisabledBySystemPolicy
    $output | Add-Member -MemberType NoteProperty -Name BTIDisabledByNoHardwareSupport -Value $SpeculationControlSettings.BTIDisabledByNoHardwareSupport
    $output | Add-Member -MemberType NoteProperty -Name KVAShadowRequired -Value $SpeculationControlSettings.KVAShadowRequired
    $output | Add-Member -MemberType NoteProperty -Name KVAShadowWindowsSupportPresent -Value $SpeculationControlSettings.KVAShadowWindowsSupportPresent
    $output | Add-Member -MemberType NoteProperty -Name KVAShadowWindowsSupportEnabled -Value $SpeculationControlSettings.KVAShadowWindowsSupportEnabled
    $output | Add-Member -MemberType NoteProperty -Name KVAShadowPcidEnabled -Value $SpeculationControlSettings.KVAShadowPcidEnabled
    $output | Add-Member -MemberType NoteProperty -Name OSMitigationRegKeySet -Value $SystemInformation.OSMitigationRegKeySet
    $output | Add-Member -MemberType NoteProperty -Name AVCompatibility -Value $SystemInformation.AVCompatibility
    $output | Add-Member -MemberType NoteProperty -Name MinVmVersionForCpuBasedMitigations -Value $SystemInformation.MinVmVersionForCpuBasedMitigations  
    $output | Add-Member -MemberType NoteProperty -Name InstalledUpdates -Value $SystemInformation.InstalledUpdates
    $output | Add-Member -MemberType NoteProperty -Name Uptime -Value $SystemInformation.Uptime
    $output | Add-Member -MemberType NoteProperty -Name ExecutionDate -Value $SystemInformation.ExecutionDate

	$output
	
}

#Set Installation Registry Key
$registryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\MeltdownSpectreReport"
$Name = "Version"
$value = "1"

if(!(Test-Path $registryPath))
  {
    New-Item -Path $registryPath -Force | Out-Null
    New-ItemProperty -Path $registryPath -Name $name -Value $value -PropertyType DWORD -Force | Out-Null
    }
 else{
    New-ItemProperty -Path $registryPath -Name $name -Value $value -PropertyType DWORD -Force | Out-Null
    }

if ($ComputerName) {
    $SessionOption = New-PSSessionOption -NoMachineProfile
    $CimSession = New-PSSession -ComputerName $ComputerName -SessionOption $SessionOption

    Invoke-Parallel -InputObject $CimSession -ScriptBlock {
        Invoke-Command -ScriptBlock $GetMeltdownStatusInformation -Session $_
    } -ImportVariable

    $CimSession | Remove-CimSession -ErrorAction SilentlyContinue
}
else {
     $result = . $GetMeltdownStatusInformation

     <#
     #Outputing results to registry
     New-ItemProperty -Path $registryPath -Name ComputerName -Value ($result).ComputerName -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name Manufacturer -Value ($result).Manufacturer -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name Model -Value ($result).Model -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name BIOS -Value ($result).BIOS -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name CPU -Value ($result).CPU -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name OperatingSystem -Value ($result).OperatingSystem -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name OSReleaseId -Value ($result).OSReleaseId -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name isHyperV -Value ($result).isHyperV -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name isTerminalServer -Value ($result).isTerminalServer -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name isDocker -Value ($result).isDocker -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name 'CVE-2017-5754 mitigated' -Value ($result).'CVE-2017-5754 mitigated' -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name 'CVE-2017-5715 mitigated' -Value ($result).'CVE-2017-5715 mitigated' -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name 'CVE-2017-5753 mitigated in Edge' -Value ($result).'CVE-2017-5753 mitigated in Edge' -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name 'CVE-2017-5753 mitigated in IE' -Value ($result).'CVE-2017-5753 mitigated in IE' -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name 'CVE-2017-5753 mitigated in Chrome' -Value ($result).'CVE-2017-5753 mitigated in Chrome' -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name 'CVE-2017-5753 mitigated in Firefox' -Value ($result).'CVE-2017-5753 mitigated in Firefox' -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name BTIHardwarePresent -Value ($result).BTIHardwarePresent -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name BTIWindowsSupportPresent -Value ($result).BTIWindowsSupportPresent -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name BTIWindowsSupportEnabled -Value ($result).BTIWindowsSupportEnabled -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name BTIDisabledBySystemPolicy -Value ($result).BTIDisabledBySystemPolicy -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name BTIDisabledByNoHardwareSupport -Value ($result).BTIDisabledByNoHardwareSupport -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name KVAShadowRequired -Value ($result).KVAShadowRequired -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name KVAShadowWindowsSupportPresent -Value ($result).KVAShadowWindowsSupportPresent -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name KVAShadowWindowsSupportEnabled -Value ($result).KVAShadowWindowsSupportEnabled -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name KVAShadowPcidEnabled -Value ($result).KVAShadowPcidEnabled -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name OSMitigationRegKeySet -Value ($result).OSMitigationRegKeySet -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name AVCompatibility -Value ($result).AVCompatibility -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name MinVmVersionForCpuBasedMitigations -Value ($result).MinVmVersionForCpuBasedMitigations -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name InstalledUpdates -Value ($result).InstalledUpdates -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name Uptime -Value ($result).Uptime -PropertyType String -Force | Out-Null
     New-ItemProperty -Path $registryPath -Name ExecutionDate -Value ($result).ExecutionDate -PropertyType String -Force | Out-Null

     #[int]$var = $result.MinVmVersionForCpuBasedMitigations
     $result
     #>
     $returnvalue = . $GetMeltdownStatusInformation
     

     return ($returnvalue).'OSMitigationRegKeySet'
}