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

Overview

This process will allow you to automate deployment of required WSUS updates in your SCCM environment that were missed by your Software Update ADR. A built in SCCM SQL report can indicate which WSUS software updates are required, but not deployed in an environment.

Utilizing my two other functions, HTML Email Report and SCCM Report to Array, you can help automate the process of detecting these updates and re-injecting them into the proper Software Update Groups which target the computers requiring them.

Fundamentals

At its core, the idea here is to automate running this report and to do something with the values that are returned. For example we can choose the report we want to target:


$ReportPath="/ConfigMgr_DGM/Software Updates - B Deployment Management/Management 2 - Updates required but not deployed"

And then choose the same parameters that we would have chosen int he GUI above;



$inputParams = @{
    "CollID"="DGM00084";
    "UpdateClass"="Security Updates";
    "Vendor"="Microsoft";
}

and then using my SQL to Array function we can store the report results as an array:


$array = Get-DMGSCCMSQLReport -inputParams $inputParams `
                               -ReportServerUrl $ReportServerUrl `
                               -ReportPath $ReportPath `
                               -ProviderMachineName $ProviderMachineName `
                               -Sitecode $Sitecode

These returned results are easily be passed into the next phase where they are automatically injected into the proper Software Update Group:


$updates = $array
$undeployedupdates=$updates | %{Get-CMSoftwareUpdate -ArticleId $_.update -Fast | ?{$_.nummissing -ge 1}} 
$PilotSoftwareUpdategroup=Get-CMSoftwareUpdateGroup -Name "Desired Software Update Group* nnn"
$undeployedupdates | %{Add-CMSoftwareUpdateToGroup -SoftwareUpdateId $_.CI_ID -SoftwareUpdateGroupName "SVR - 2 - Production Servers Updates - All other Products* nnn"}

You can then choose the Software Update Group you want these automatically injected inside of:


#Multiple Arrays
Get-DMGEmailReport `
    -Arrays $OutputArrays `
    -ReportTitle "Updates Required but Not Deployed Report" `
    -from "SCCMSQLReports@corporation.com" `
    -To "c-dmaiolo@corporation.com" `
    -subject "SCCM Report: Required But Not Deployed (Not Superseded, Not Expired, Not Security Only)"

For this example let’s choose to also have the results first emailed out utilizing my HTML Email Function. This generates an email, indicating which updates were included:

required but not deployed

PowerShell Invocation


Import-Module \\scriptserver\scripts\DMGSCCM\Get-DMGSCCMSQLReport\Get-DMGSCCMSQLReport.psm1 -Force
Import-Module \\scriptserver\scripts\Get-DMGEmailReport\Get-DMGEmailReport.psm1 -Force

#Set Universal Parameters for this Report
$ReportServerUrl="http://sccmsqlrserver/ReportServer"
$ReportPath="/ConfigMgr_DGM/Software Updates - B Deployment Management/Management 2 - Updates required but not deployed"

#Create Some Arrays Of Data To Display in Report. You can create as many as you want.
$OutputArrays = @()
$ProviderMachineName = "sccmsqlrserver.corp.corporation.com"
$Sitecode = "DGM"

Set-Location $Sitecode":"

#Array1
$inputParams = @{
    "CollID"="DGM00084";
    "UpdateClass"="Security Updates";
    "Vendor"="Microsoft";
}

$array = Get-DMGSCCMSQLReport -inputParams $inputParams `
                               -ReportServerUrl $ReportServerUrl `
                               -ReportPath $ReportPath `
                               -ProviderMachineName $ProviderMachineName `
                               -Sitecode $Sitecode
Set-Location $Sitecode":"  
Write-Host "Gonna take a while..."                                          
$arrayresult = $array | %{Get-CMSoftwareUpdate -ArticleId $_.Details_Table0_Title -Fast| ?{$_.nummissing -ge 1 -and $_.IsExpired -eq $FALSE -and $_.isSuperseded -eq $FALSE -and $_.LocalizedDisplayName -notlike "*Security Only*"}} | `
                               Select ArticleID,LocalizedDisplayName,NumMissing,NumPresent,IsSuperseded,IsExpired -Unique | Sort-Object -Descending -Property NumMissing

$output = [PSCustomObject] @{
'Message' = "These are all of the Windows Updates that are required but not deplyoyed for All Servers.";
'Title' = "All Production Servers with Maintenanace Window (Security Updates): Required But Not Deployed";
'Color' = "Red";
'Array' = $arrayresult
}

if ($output.Array -ne $NULL){$OutputArrays+=$output}


#Array2
$inputParams = @{
    "CollID"="DGM00084";
    "UpdateClass"="Critical Updates";
    "Vendor"="Microsoft";
}

$array = Get-DMGSCCMSQLReport -inputParams $inputParams `
                               -ReportServerUrl $ReportServerUrl `
                               -ReportPath $ReportPath `
                               -ProviderMachineName $ProviderMachineName `
                               -Sitecode $Sitecode

Set-Location $Sitecode":"                                               
Write-Host "Gonna take a while..."                                          
$arrayresult = $array | %{Get-CMSoftwareUpdate -ArticleId $_.Details_Table0_Title -Fast| ?{$_.nummissing -ge 1 -and $_.IsExpired -eq $FALSE -and $_.isSuperseded -eq $FALSE -and $_.LocalizedDisplayName -notlike "*Security Only*"}} | `
                               Select ArticleID,LocalizedDisplayName,NumMissing,NumPresent,IsSuperseded,IsExpired -Unique | Sort-Object -Descending -Property NumMissing

$output = [PSCustomObject] @{
'Message' = "These are all of the Windows Updates that are required but not deplyoyed for All Servers.";
'Title' = "All Production Servers with Maintenanace Window (Security Updates): Required But Not Deployed";
'Color' = "Red";
'Array' = $arrayresult
}

if ($output.Array -ne $NULL){$OutputArrays+=$output}

#Array3
$inputParams = @{
    "CollID"="DGM00085";
    "UpdateClass"="Security Updates";
    "Vendor"="Microsoft";
}

$array = Get-DMGSCCMSQLReport -inputParams $inputParams `
                               -ReportServerUrl $ReportServerUrl `
                               -ReportPath $ReportPath `
                               -ProviderMachineName $ProviderMachineName `
                               -Sitecode $Sitecode

Set-Location $Sitecode":"                                               
Write-Host "Gonna take a while..."                                          
$arrayresult = $array | %{Get-CMSoftwareUpdate -ArticleId $_.Details_Table0_Title -Fast| ?{$_.nummissing -ge 1 -and $_.IsExpired -eq $FALSE -and $_.isSuperseded -eq $FALSE -and $_.LocalizedDisplayName -notlike "*Security Only*"}} | `
                               Select ArticleID,LocalizedDisplayName,NumMissing,NumPresent,IsSuperseded,IsExpired -Unique | Sort-Object -Descending -Property NumMissing

$output = [PSCustomObject] @{
'Message' = "These are all of the Windows Updates that are required but not deplyoyed for All Servers.";
'Title' = "All Production Servers with Maintenanace Window (Security Updates): Required But Not Deployed";
'Color' = "Red";
'Array' = $arrayresult
}

if ($output.Array -ne $NULL){$OutputArrays+=$output}

#Array4
$inputParams = @{
    "CollID"="DGM00085";
    "UpdateClass"="Critical Updates";
    "Vendor"="Microsoft";
}

$array = Get-DMGSCCMSQLReport -inputParams $inputParams `
                               -ReportServerUrl $ReportServerUrl `
                               -ReportPath $ReportPath `
                               -ProviderMachineName $ProviderMachineName `
                               -Sitecode $Sitecode

Set-Location $Sitecode":"                                               
Write-Host "Gonna take a while..."                                          
$arrayresult = $array | %{Get-CMSoftwareUpdate -ArticleId $_.Details_Table0_Title -Fast| ?{$_.nummissing -ge 1 -and $_.IsExpired -eq $FALSE -and $_.isSuperseded -eq $FALSE -and $_.LocalizedDisplayName -notlike "*Security Only*"}} | `
                               Select ArticleID,LocalizedDisplayName,NumMissing,NumPresent,IsSuperseded,IsExpired -Unique | Sort-Object -Descending -Property NumMissing

$output = [PSCustomObject] @{
'Message' = "These are all of the Windows Updates that are required but not deplyoyed for All Servers.";
'Title' = "All Production Servers with Maintenanace Window (Security Updates): Required But Not Deployed";
'Color' = "Red";
'Array' = $arrayresult
}

if ($output.Array -ne $NULL){$OutputArrays+=$output}

#Multiple Arrays
Get-DMGEmailReport `
    -Arrays $OutputArrays `
    -ReportTitle "Updates Required but Not Deployed Report" `
    -from "SCCMSQLReports@corporation.com" `
    -To "c-dmaiolo@corporation.com" `
    -subject "SCCM Report: Required But Not Deployed (Not Superseded, Not Expired, Not Security Only)"

Overview

I created this script, Get-DMGSCCMNewDeployments, to allow you to send out an email report of new SCCM deployments in your environment. This script uses native SCCM methods to build an automated report that sends an easy to read “what was deployed this week” report out to SCCM administrators.

Methodology

The report is compiled one array at a time, then combined into an array of arrays that is fed into my HTML email script.

Generating an Automated Report

This report loops through all of the available deployments that were created within the last seven days and emails the results to an email address:

If you would like to change the date range, simply update the values -MinDaysOld and -MaxDaysOld in the array element that is in the invocation function when calling the script:


'Array' = Get-DMGSCCMNewDeployments -MinDaysOld 0 -MaxDaysOld 7 -PercentSuccessThreshold 1 -NumberOfTargetedThreshold 0 -FeatureType Baseline -ProviderMachineName $ProviderMachineName -Sitecode $Sitecode;

PowerShell Invocation Function


Import-Module \\scriptserver\scripts\Get-DMGEmailReport\Get-DMGEmailReport.psm1 -Force
Import-Module \\scriptserver\Scripts\DMGSCCM\Get-DMGSCCMNewDeployments\Get-DMGSCCMNewDeployments.psm1 -Force

#Create Some Arrays Of Data To Display in Report. You can create as many as you want.
$OutputArrays = @()
$ProviderMachineName = "SCCMSERVER.corp.corporation.com" #Enter Your SCCM Server Here
$Sitecode = "NNN" #Enter Your Site Code Here
   
#Array1
$output = [PSCustomObject] @{
'Message' = "These are all of the new Task Sequence deployments, and the collections they were deployed to, that were created within the last seven days. As a reminder, Task Sequences should not be used to deploy applications outside of an Operating System deployment.";
'Title' = "Task Sequence Deployments`: New This Week!";
'Color' = "Red";
'Array' = Get-DMGSCCMNewDeployments -MinDaysOld 0 -MaxDaysOld 7 -PercentSuccessThreshold 1 -NumberOfTargetedThreshold 0 -FeatureType TaskSequence -ProviderMachineName $ProviderMachineName -Sitecode $Sitecode;
}
if ($output.Array -ne $NULL){$OutputArrays+=$output}

#Array2
$output = [PSCustomObject] @{
'Message' = "These are all of the new Package deployments, and the collections they were deployed to, that were created within the last seven days. As a reminder, packages have been depreciated in SCCM, in favor of using Applications.";
'Title' = "Package Deployments`: New This Week!";
'Color' = "Red";
'Array' = Get-DMGSCCMNewDeployments -MinDaysOld 0 -MaxDaysOld 7 -PercentSuccessThreshold 1 -NumberOfTargetedThreshold 0 -FeatureType Package -ProviderMachineName $ProviderMachineName -Sitecode $Sitecode;
}
if ($output.Array -ne $NULL){$OutputArrays+=$output}

#Array3
$output = [PSCustomObject] @{
'Message' = "These are all of the new Application deployments, and the collections they were deployed to, that were created within the last seven days.";
'Title' = "Application Deployments`: New This Week!";
'Color' = "Black";
'Array' = Get-DMGSCCMNewDeployments -MinDaysOld 0 -MaxDaysOld 7 -PercentSuccessThreshold 1 -NumberOfTargetedThreshold 0 -FeatureType Application -ProviderMachineName $ProviderMachineName -Sitecode $Sitecode;
}
if ($output.Array -ne $NULL){$OutputArrays+=$output}

#Array4
$output = [PSCustomObject] @{
'Message' = "These are all of the new Windows Update deployments, and the collections they were deployed to, that were created within the last seven days.";
'Title' = "Windows Update Deployments`: New This Week!";
'Color' = "Black";
'Array' = Get-DMGSCCMNewDeployments -MinDaysOld 0 -MaxDaysOld 7 -PercentSuccessThreshold 1 -NumberOfTargetedThreshold 0 -FeatureType Update -ProviderMachineName $ProviderMachineName -Sitecode $Sitecode;
}
if ($output.Array -ne $NULL){$OutputArrays+=$output}

#Array5
$output = [PSCustomObject] @{
'Message' = "These are all of the new Configuration Baseline deployments, and the collections they were deployed to, that were created within the last seven days.";
'Title' = "Configuration Baseline Deployments`: New This Week!";
'Color' = "Black";
'Array' = Get-DMGSCCMNewDeployments -MinDaysOld 0 -MaxDaysOld 7 -PercentSuccessThreshold 1 -NumberOfTargetedThreshold 0 -FeatureType Baseline -ProviderMachineName $ProviderMachineName -Sitecode $Sitecode;
}
if ($output.Array -ne $NULL){$OutputArrays+=$output}


#Multiple Arrays
Get-DMGEmailReport `
    -Arrays $OutputArrays `
    -ReportTitle "Last Seven Days SCCM Deployments Report" `
    -from "SCCMDeployments@corporation.com" `
    -To "serverteam@corporation.com","desktopteam@corporation.com","ddinh@corporation.com" `
    -subject "This Week's New SCCM Deployments"

PowerShell Function


<#
.SYNOPSIS
  Generates an array of SCCM objects that are new deployments
.NOTES
  Version:        1.0
  Author:         David Maiolo
  Creation Date:  2018-01-10
  Purpose/Change: Initial script development

#>

#---------------------------------------------------------[Initialisations]--------------------------------------------------------

function Get-DMGSCCMNewDeployments {
    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateRange(1,10000)]
        [Int] $MaxDaysOld,
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateRange(0,10000)]
        [Int] $MinDaysOld,
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateRange(0,1)]
        [Float] $PercentSuccessThreshold,
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateRange(0,10000)]
        [int] $NumberOfTargetedThreshold,

        [Parameter(Position=2,Mandatory=$false,ValueFromPipeline=$true)]
        [ValidateSet("Application","Package","Update","Baseline","TaskSequence")]
        [String]$FeatureType,

        [Parameter(Position=3,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$ProviderMachineName,

        [Parameter(Position=4,Mandatory=$true,ValueFromPipeline=$true)]
        [ValidateLength(3,3)]
        [String]$Sitecode

    )

    #Check which feature type was selected and convert to the number Get-CMDeployments uses
    if([bool]($MyInvocation.BoundParameters.Keys -contains 'FeatureType')){
           switch ($FeatureType)
           {
               'Application' {$FeatureTypeOutput=1}
               'Package' {$FeatureTypeOutput=2}
               'Update' {$FeatureTypeOutput=5}
               'Baseline' {$FeatureTypeOutput=6}
               'TaskSequence' {$FeatureTypeOutput=7}
           }
    }

    #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):\"

    #Get date for comparison
    $MaxDaysOldAgo=(Get-Date).AddDays(-$MaxDaysOld)
    $MinDaysOldAgo=(Get-Date).AddDays(-$MinDaysOld)

    Write-Host "Processing $($FeatureType)s..."

    #Get deployment matching featuretype and date range
    if ($FeatureType){
        $DeploymentsNewerThanDaysAgo = Get-CMDeployment | Where-Object {$_.DeploymentTime -gt $MaxDaysOldAgo -and $_.DeploymentTime -lt $MinDaysOldAgo -and $_.FeatureType -eq $FeatureTypeOutput}
    }else{
        $DeploymentsNewerThanDaysAgo = Get-CMDeployment | Where-Object {$_.DeploymentTime -gt $MaxDaysOldAgo -and $_.DeploymentTime -lt $MinDaysOldAgo}
    }

    #Created a Calculated Value of the Percentage of Succesful Deployments and add additional Properties
    $DMGSCCMNewDeployments = $DeploymentsNewerThanDaysAgo | Select-Object -Property `
        @{Name = 'Deployment Name'; Expression = {$_.ApplicationName}},`
        @{Name = 'Deployed To'; Expression = {$_.CollectionName}},`
        @{Name = 'Deployment Time'; Expression = {$_.DeploymentTime}},`
        @{Name = 'Percent Success'; Expression = {[math]::Round(($_.Properties.NumberSuccess/$_.Properties.NumberTargeted),2)*100}},`
        @{Name = 'Number Targeted'; Expression = {$_.Properties.NumberTargeted}},`
        @{Name = 'Success'; Expression = {$_.Properties.NumberSuccess}},`
        @{Name = 'In Progress'; Expression = {$_.Properties.NumberInProgress}},`
        @{Name = 'Unknowns'; Expression = {$_.Properties.NumberUnknown}}

    #Calculate where the percent success than the supplied values
    $DMGSCCMNewDeployments = $DMGSCCMNewDeployments | Where-Object {$_."Percent Success" -le ($PercentSuccessThreshold*100) -and $_."Number Targeted" -gt $NumberOfTargetedThreshold} | Sort-Object -Property 'Deployment Name'

    #Return the array
    return $DMGSCCMNewDeployments

}

Overview

This script will allow you to take a standard SCCM SQL Report and output the report, automatically, to either a .CSV file or to an array for use in a pipeline.

Workflow

This standard SCCM SQL report can be run which would allow you to see all of the software updates that are required but not deployed via WSUS.

Software Updates – Updates required but not deployed

Simply running the report gives you an indication of what variables are required to run this report. When fed into this function, the report will run automatically with these values chosen and the results can be either output to the pipeline as an array or to a .CSV file for easy emailing.


Import-Module \\scriptserver\scripts\DMGSCCM\Get-DMGSCCMSQLReport\Get-DMGSCCMSQLReport.psm1 -Force


#Set Universal Parameters for this Report
$ReportServerUrl="http://sccmsqlrserver/ReportServer"
$ReportPath="/ConfigMgr_DGM/Software Updates - B Deployment Management/Management 2 - Updates required but not deployed" #Include your report path here - this is a sample only

#Create Array Of Data To Display in Report.
$OutputArrays = @()
$ProviderMachineName = "sccmsqlrserver.corp.corporation.com" #enter your sccm sql server here
$Sitecode = "DGM" #enter your site code here

Set-Location $Sitecode":"

#Array1
$inputParams = @{
    "CollID"="DGM00084"; #These are sample values
    "UpdateClass"="Security Updates";
    "Vendor"="Microsoft";
}

$array = Get-DMGSCCMSQLReport -inputParams $inputParams `
                               -ReportServerUrl $ReportServerUrl `
                               -ReportPath $ReportPath `
                               -ProviderMachineName $ProviderMachineName `
                               -Sitecode $Sitecode
Set-Location $Sitecode":"  

#Generate Array with All Production Servers with Maintenance Window (Security Updates): Required But Not Deployed                                        
$arrayresult = $array | %{Get-CMSoftwareUpdate -ArticleId $_.Details_Table0_Title -Fast| ?{$_.nummissing -ge 1 -and $_.IsExpired -eq $FALSE -and $_.isSuperseded -eq $FALSE -and $_.LocalizedDisplayName -notlike "*Security Only*"}} | `
                               Select ArticleID,LocalizedDisplayName,NumMissing,NumPresent,IsSuperseded,IsExpired -Unique | Sort-Object -Descending -Property NumMissing

As you can see in the example, we take the results from a standard SQL report, transform them into an array as $array, and simultaneously pipe them to Get-CMSoftwareUpdate for further processing as $arrayresult.

Logging Results

This function has standard capabilities to output its status a log file which is fully compatible with CMTrace.exe

PowerShell Function


<#
.SYNOPSIS
  Generates an array of a SCCM SQL Report
.NOTES
  Version:        1.0
  Author:         David Maiolo
  Creation Date:  2018-01-11
  Purpose/Change: Initial script development

#>

#---------------------------------------------------------[Initialisations]--------------------------------------------------------
 function Get-DMGSCCMSQLReport
 {
    param(
        [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
        $inputParams,
        [Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)]
        $ReportServerUrl,
        [Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$true)]
        $ReportPath,
        [Parameter(Position=3,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$ProviderMachineName,
        [Parameter(Position=4,Mandatory=$true,ValueFromPipeline=$true)]
        [String]$Sitecode
    )


    #Set Logging Varibales
    $invocation = (Get-Variable MyInvocation -Scope 1).Value
    $ScriptDirectory = Split-Path $invocation.MyCommand.Path
    $ScriptName = ($MyInvocation.MyCommand.Name)+".psm1"
    $LogName = ($MyInvocation.MyCommand.Name)+".log"
    $LogFile = Join-Path $ScriptDirectory $LogName
    $ScriptFile = Join-Path $ScriptDirectory $ScriptName
    $ReportDate = Get-Date 

    #Set CSV Output Variables
    $CSVOutputName = ($MyInvocation.MyCommand.Name)+".csv"
    $CSVOutputFile = Join-Path $ScriptDirectory $CSVOutputName

    #Log Start of Function
    New-DMGCMTraceLog -message ("Starting Logging for $ScriptName") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile

    #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) {
        New-PSDrive -Name $SiteCode -PSProvider CMSite -Root $ProviderMachineName @initParams
    }

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


    # add assembly 
    Add-Type -AssemblyName "Microsoft.ReportViewer.WinForms, Version=12.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"

    # if the path exists, will error silently and continue 
    New-Item -ItemType Directory -Path $baseFolder -ErrorAction SilentlyContinue | Out-Null

    $rv = New-Object Microsoft.Reporting.WinForms.ReportViewer

    # report Server Properties 
    $rv.ServerReport.ReportServerUrl = $ReportServerUrl
    $rv.ServerReport.ReportPath = $ReportPath
    $rv.ProcessingMode = "Remote"

    # set up report parameters 
    $params = $null

    #create an array based on how many incoming parameters 
    $params = New-Object 'Microsoft.Reporting.WinForms.ReportParameter[]' $inputParams.Count

    $i = 0 
    foreach ($p in $inputParams.GetEnumerator()) 
    { 
        $params[$i] = New-Object Microsoft.Reporting.WinForms.ReportParameter($p.Name, $p.Value, $true) 
        $i++ 
    } 

    # set the parameters 
     Write-Host "Setting Parameters..."
    $rv.ServerReport.SetParameters($params) 
    $rv.ShowParameterPrompts = $false 
    $rv.RefreshReport() 
    $rv.ServerReport.Refresh()

    Write-Host "The Parameters Were Applied..."

    # set rendering parameters 
    $mimeType = $null 
    $encoding = $null 
    $extension = $null 
    $streamids = $null 
    $warnings = $null

    # render the SSRS report in CSV 
    $bytes = $null 
    $bytes = $rv.ServerReport.Render("CSV", 
    $null, 
    [ref] $mimeType, 
    [ref] $encoding, 
    [ref] $extension, 
    [ref] $streamids, 
    [ref] $warnings)

    Set-Location C:\

    # save the report to a file
    $fileStream = New-Object System.IO.FileStream($CSVOutputFile, [System.IO.FileMode]::OpenOrCreate) 
    $fileStream.Write($bytes, 0, $bytes.Length) 
    $fileStream.Close()

    New-DMGCMTraceLog -message ("File Exported`: $CSVOutputFile") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile

    # Re-import file and remove first three lines

    get-content -LiteralPath $CSVOutputFile|
        select -Skip 3 |
        set-content "$CSVOutputFile-temp"
    move "$CSVOutputFile-temp" $CSVOutputFile -Force
    #Log Sent Email
    New-DMGCMTraceLog -message ("File Imported (First 3 Lines Removed)`: $CSVOutputFile") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile

    $Finalvalues = Import-CSV -LiteralPath $CSVOutputFile

    #Log End Of Function
    New-DMGCMTraceLog -message ("End Logging for $ScriptName") -component "Main()" -type 1 -ScriptName $ScriptName -LogFile $LogFile -ScriptFile $ScriptFile

    return $Finalvalues

}

Overview

At times it is important to determine if a client machine has a certain certificate installed from a certificate template. I developed this script, Get-DMGCertificateTemplateExistance, to to detect if a certificate was created from a particular template name. It could be run stand-alone or is also deployable as an SCCM configuration item/baseline and will return the desired results in a true / false fashion.

Instructions

Add the namme of the certificate template you would like to check within the Invoke-DMGCertificateTemplateExistance function and then run the PowerShell script. If the computer you run the script on has a certificate that was created from the template you provided, the script will return true, and it will return false otherwise.


function Invoke-DMGCertificateTemplateExistance{
    $CertificateName = 'Display Name of Certificate Template Certificate Was Created From'
    Get-DMGCertificateTemplateExistance -CertificateName $CertificateName
}

Detecting Certificate Template Compliance in SCCM

Deploy this to your required machines as a configuration baseline compliance item. Deployed to this sample machine, we have configured the function to look for a Cisco ISE certificate. Here we can see the machine reports compliance.

cid:image003.png@01D39C74.153A0660

PowerShell Script: Get-DMGCertificateTemplateExistance.ps1


<#

.SYNOPSIS
  Determines if a certificate exists on the local machine that matches the template name
.NOTES
  Version:        1.0
  Author:         David Maiolo
  Creation Date:  2018-02-02
  Purpose/Change: Initial script development

#>

function Get-DMGCertificateTemplateExistance{
    [CmdletBinding()]
    param(
    [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
    [String]$CertificateName
    )

    #Set Certificate Template Existance Count
    $i = 0
    #Get All of the local machine certificates
    $cert = $null
    $certs = $null
    $certs = get-childitem cert:\localmachine\my

    #Loop through each certificate
    foreach ($cert in $certs){ 
        $temp = $null

        #See if certificate associated with Microsoft Chryptogrophy:  szOID_ENROLL_CERTTYPE_EXTENSION
        $temp = $cert.Extensions | Where-Object{$_.Oid.Value -eq "1.3.6.1.4.1.311.20.2"}
        if(!$temp){
            #Else see if certificate associated with Microsoft CertSrv Infrastructure: Certificate template extension (v2) szOID_CERTIFICATE_TEMPLATE
            $temp = $cert.Extensions | Where-Object{$_.Oid.Value -eq "1.3.6.1.4.1.311.21.7"}
         }
         
         #Create a New Value, Template, and see if it mateches the template name we are looking for
         if($temp){
            $cert | Add-Member -Name Template -MemberType NoteProperty -Value $temp.Format(1)
        
            #If the template name is found, incrememnt the Certificate Template Existance Count
            if ($cert.template.contains($CertificateName)){
                $i++
                break;
            }
        }
    }

    #If the Certificate Template Existance Count is greater than one, we found a certificate with our template
    if ($i -gt 0){return $true; break}else{return $false}
}

function Invoke-DMGCertificateTemplateExistance{
    $CertificateName = 'Display Name of Certificate Template Certificate Was Created From'
    Get-DMGCertificateTemplateExistance -CertificateName $CertificateName
}

Invoke-DMGCertificateTemplateExistance

Overview

At times, SCCM client machines may lose the ability to communicate properly with the SCCM site server due to Kerberos authentication errors. You will see these errors in Deployment Statuses or other times invoking PowerShell scripts on remote clients.

This script attempts to invoke a generic script remotely on an imported set of SCCM clients and determine if there is a Kerberos authentication error:

 Invoke-Command -ComputerName $h -ScriptBlock { Test-Connection SCCMSERVER -Quiet -Count 1 } -ErrorAction Stop 

If the invocation returns the exception error System.Management.Automation.Remoting.PSRemotingTransportException you can assume there is an error and we can remotely instruct a CLI through WinRM:

 $command = "C:\ADMIN`\PSTools\PsExec.exe \\$h /s /accepteula /nobanner cmd /c `"Winrm quickconfig /q`"" 

Therefor it is a prerequisite that you have PsExec.exe in the C:\ADMIN\PSTools\ directory of the PC before running the script, as shown above.

The script will also try to resolve DNS issues that may be related to the error.

Fixing Kerberos Errors on an Imported List of Computers

You can import a generic list of computers via a CSV that is in the following format:

 Fix-DGMKerberosError –CSV \\Pathto\the.csv 

Logging Output

The function outputs its progress to a log file and is fully compatible with CMTrace.exe.

PowerShell Function

 
<#
.Synopsis
   Fix SCCM Kerberos Errors
.DESCRIPTION
   The tool, Fix-DMGKerberosError.ps1 was written by David Maiolo which will attempt to fix Kerberos Authentican Issues on remote computers.
.EXAMPLE
   Fix-DMGKerberosError -CSVFile kerberos_error_import.csv
.EXAMPLE
   Fix-DMGKerberosError -Hostname LT061222
#>


function LogIt
{
  param (
  [Parameter(Mandatory=$true)]
  $message,
  [Parameter(Mandatory=$true)]
  $component,
  [Parameter(Mandatory=$true)]
  $type )

  switch ($type)
  {
    1 { $type = "Info" }
    2 { $type = "Warning" }
    3 { $type = "Error" }
    4 { $type = "Verbose" }
  }

  if (($type -eq "Verbose") -and ($Global:Verbose))
  {
    $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile)
    Write-Host $message
  }
  elseif ($type -ne "Verbose")
  {
    $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile)
    if ($type -eq 'Info') { Write-Host $message }
    if ($type -eq 'Warning') { Write-Host $message -ForegroundColor Yellow}
    if ($type -eq 'Error') { Write-Host $message -ForegroundColor Red}
    

  }
  if (($type -eq 'Warning') -and ($Global:ScriptStatus -ne 'Error')) { $Global:ScriptStatus = $type }
  if ($type -eq 'Error') { $Global:ScriptStatus = $type }

  if ((Get-Item $Global:LogFile).Length/1KB -gt $Global:MaxLogSizeInKB)
  {
    $log = $Global:LogFile
    Remove-Item ($log.Replace(".log", ".lo_"))
    Rename-Item $Global:LogFile ($log.Replace(".log", ".lo_")) -Force
  }
} 

function GetScriptDirectory
{
  $invocation = (Get-Variable MyInvocation -Scope 1).Value
  Split-Path $invocation.MyCommand.Path
} 

function Fix-DMGKerberosError
{
    [CmdletBinding()]
    [Alias()]
    [OutputType([int])]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0,
                   ParameterSetName='Parameter Set 1')]
                   [ValidateScript({(Test-Path $_)})]
                   $CSVFile,
        # Param2 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0,
                   ParameterSetName='Parameter Set 2')]
                   [ValidateScript({(Get-ADComputer -Identity $_).objectclass -eq 'computer' })]
                   [String]$Hostname
    )

    Begin
    {
        $path = (get-item -Path .).FullName 
        
        if ($CSVFile -ne $null){
            Write-Host Importing $CSVFile...
            $csv = import-csv "$CSVFile"
        }else{
            $csv = [PSCustomObject]@{
                Hostname = $Hostname}
        }
        Write-Host ==========================================
        Write-Host SCCM Kerberos Remote Execution Fix Tool
        Write-Host ==========================================
        Write-Host "v0.1 (2017-12-20) by dmaiolo"
        LogIt -message ("Starting Logging for Fix-DMGKerberosError") -component "Main()" -type 1 
    }
    Process
    {
    

    $computers = @();
        
    $csv | foreach-object {
        $h = $_.Hostname
    

        if(Test-Connection -ComputerName $h -Count 1 -Quiet){

            Write-Host "$h`: Invoking Generic Command (Test-Connection SCCMSERVER -Quiet -Count 1)..."
            Try{
                Invoke-Command -ComputerName $h -ScriptBlock { Test-Connection SCCMSERVER -Quiet -Count 1 } -ErrorAction Stop
                LogIt -message ("$h`: Generic Command (Test-Connection SCCMSERVER -Quiet -Count 1) Ran Succesfully. No Fix Required.") -component "Main()" -type 1
            }
            Catch [System.Management.Automation.Remoting.PSRemotingTransportException]{
                LogIt -message ("$h`: Kerberos Issue Detected. Fix Required") -component "Main()" -type 2
                Write-Host "$h`: Attempting to Repair WinRM for Kerberos Authentication..."
                $command = "C:\ADMIN`\PSTools\PsExec.exe \\$h /s /accepteula /nobanner cmd /c `"Winrm quickconfig /q`""
                if($expression = Invoke-Expression -Command:"$command" -ErrorAction Stop) {
                    LogIt -message ("$h`: WinRM Repair Ran Succesfully for Kerberos Authentication") -component "Main()" -type 1
                    }
                else{
                    LogIt -message ("$h`: WinRM Repair NOT Succesfull for Kerberos Authentication") -component "Main()" -type 3
                    LogIt -message ("$h`: Attempting DNS Repair by running PsExec for ipconfig /registerdns against the IP Address... ") -component "Main()" -type 1
                    try{
                        $IPv4AddressPinged = (Test-Connection -ComputerName $h -Count 1 -ErrorAction SilentlyContinue).IPV4Address.IPAddressToString
                        LogIt -message ("$h`: Found a valid IP Address To Invoke-Expression against ($IPv4AddressPinged).") -component "Main()" -type 1
                        $RealHostName = (Resolve-DnsName $IPv4AddressPinged).NameHost
                        LogIt -message ("$h`: Your DNS has accociated $h to $IPv4AddressPinged, but it acually belongs to $RealHostName. This is part of your problem.") -component "Main()" -type 1
                        
                    }
                    catch{
                        LogIt -message ("$h`: No Valid IP Address for this host could be determined.") -component "Main()" -type 3
                        $RealHostName = $false
                        $IPv4AddressPinged = $false
                    }
                    if ($IPv4AddressPinged){
                        $command2 = "C:\ADMIN\PSTools\PsExec.exe \\$IPv4AddressPinged /s /accepteula /nobanner cmd /c `"ipconfig /registerdns`""
                        if(Invoke-Expression -Command:"$command2"){
                            LogIt -message ("$h`: ($RealHostName) Succesfully Ran $command2.") -component "Main()" -type 1
                            Write-Host "$h`: ($RealHostName) Attempting to Repair WinRM for Kerberos Authentication against IP Address ($IPv4AddressPinged)..."
                            $command3 = "C:\ADMIN`\PSTools\PsExec.exe \\$IPv4AddressPinged /s /accepteula /nobanner cmd /c `"Winrm quickconfig /q`""
                            if($expression2 = Invoke-Expression -Command:"$command3" -ErrorAction Stop) {
                                LogIt -message ("$h`: ($RealHostName) WinRM Repair Ran Succesfully for Kerberos Authentication against IP Address ($IPv4AddressPinged)") -component "Main()" -type 1
                            }
                            else{
                                LogIt -message ("$h`: ($RealHostName) WinRM Repair NOT Succesfull for Kerberos Authentication against IP Address ($IPv4AddressPinged)") -component "Main()" -type 3
                               }
                        }
                        else{
                            LogIt -message ("$h`: ($RealHostName) Unable to Run $command2.") -component "Main()" -type 3
                        }
                    }
                 }
            }
            Catch{
                LogIt -message ("$h`: Generic Command (Test-Connection SCCMSERVER -Quiet -Count 1) Ran Into a Non-Kerberos Issue. Fix Unknown.") -component "Main()" -type 3
            }

        }
        else{
            LogIt -message ("$h`: is offline.") -component "Main()" -type 2
        }
       }
    }
    End
    {
       Write-Host ===============================================================
       Write-Host Log File of Results Generated at $Global:LogFile VIEW WITH CMTRACE.EXE
       LogIt -message ("Ending Logging for Fix-DMGKerberosError") -component "Main()" -type 1
    }
}

$VerboseLogging = "true"
[bool]$Global:Verbose = [System.Convert]::ToBoolean($VerboseLogging)
#$Global:LogFile = Join-Path (GetScriptDirectory) "Fix-DMGKerberosError_$(Get-Date -Format dd-MM-yyyy).log"
$Global:LogFile = Join-Path (GetScriptDirectory) "Fix-DMGKerberosError.log"
$Global:MaxLogSizeInKB = 10240
$Global:ScriptName = 'Fix-DMGKerberosError.ps1' 
$Global:ScriptStatus = 'Success'

Overview

The Local Administrator Password Solution is a great tool to help you manage local passwords on your corporate network. However, due to the non-persistant nature of some environments, such as Citrix VDI, it can become a security vulnerability as the local password will always be set back the original password set when the VDI was persistent.

I created this script, Reset-LAPSPasswordExpiration.ps1, which aims to solve this problem by randomly generating a password upon either VDI startup or shutdown and synchronizing those results with the LAPS password store in Active Directory. This requires RSAT to be on the Citrix server because it uses Set-ADComputer.

How To Use This Script

The script can either be run remotely or on the Citrix machine. I recommend using the local start-up procedure as it is most effective. However, I did integrate other abilities if you wish to you modify its deployment in your environment. This script requires the proper version of RSAT be installed on the Citrix machine.

If run it locally on the Citrix server at startup (recommended way):


  Reset-LAPSPasswordExpiration -RunOnLocalHost -logic Startup 

If run local on the Citrix server at shutdown:


  Reset-LAPSPasswordExpiration -RunOnLocalHost -logic Shutdown 

To run against a batch of Citrix servers remotely:


  Reset-LAPSPasswordExpiration -CSVFile Reset-LAPSPasswordExpiration-Import.csv -Logic Shutdown 

To run against a single server remotely:


  Reset-LAPSPasswordExpiration -Hostname CITRIXSERVER001 -Logic Shutdown 

Sample Output:

Logging Output

The script has built in logging functionality which is fully compatible with CMTrace.exe

PowerShell Function: Reset-LAPSPasswordExpiration


<#
.Synopsis
   Reset the LAPS Password Expiration Date
.DESCRIPTION
   The tool, Reset-LAPSPasswordExpiration.ps1 was written by David Maiolo which will reset the LAPS Password expiration date. Useful on non-persistent VDIs
.EXAMPLE
   Reset-LAPSPasswordExpiration -CSVFile laps_computers_import.csv -Logic Startup
.EXAMPLE
   Reset-LAPSPasswordExpiration -Hostname LT061222 -Logic Shutdown
.EXAMPLE
   Reset-LAPSPasswordExpiration -RunOnLocalHost -logic Startup
#>


function New-DGMCMTraceLog
{
  param (
  [Parameter(Mandatory=$true)]
  $message,
  [Parameter(Mandatory=$true)]
  $component,
  [Parameter(Mandatory=$true)]
  $type )

  switch ($type)
  {
    1 { $type = "Info" }
    2 { $type = "Warning" }
    3 { $type = "Error" }
    4 { $type = "Verbose" }
  }

  if (($type -eq "Verbose") -and ($Global:Verbose))
  {
    $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile)
    Write-Host $message
  }
  elseif ($type -ne "Verbose")
  {
    $toLog = "{0} `$$<{1}><{2} {3}>" -f ($type + ":" + $message), ($Global:ScriptName + ":" + $component), (Get-Date -Format "MM-dd-yyyy"), (Get-Date -Format "HH:mm:ss.ffffff"), $pid
    $toLog | Out-File -Append -Encoding UTF8 -FilePath ("filesystem::{0}" -f $Global:LogFile)
    if ($type -eq 'Info') { Write-Host $message }
    if ($type -eq 'Warning') { Write-Host $message -ForegroundColor Yellow}
    if ($type -eq 'Error') { Write-Host $message -ForegroundColor Red}
    

  }
  if (($type -eq 'Warning') -and ($Global:ScriptStatus -ne 'Error')) { $Global:ScriptStatus = $type }
  if ($type -eq 'Error') { $Global:ScriptStatus = $type }

  if ((Get-Item $Global:LogFile).Length/1KB -gt $Global:MaxLogSizeInKB)
  {
    $log = $Global:LogFile
    Remove-Item ($log.Replace(".log", ".lo_"))
    Rename-Item $Global:LogFile ($log.Replace(".log", ".lo_")) -Force
  }
} 

function GetScriptDirectory
{
  $invocation = (Get-Variable MyInvocation -Scope 1).Value
  Split-Path $invocation.MyCommand.Path
} 

function Reset-LAPSPasswordExpiration
{
    [CmdletBinding()]
    [Alias()]
    [OutputType([int])]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=0,
                   ParameterSetName='CSV File')]
                   [ValidateScript({(Test-Path $_)})]
                   $CSVFile,
        # Param2 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=1,
                   ParameterSetName='Single Computer')]
                   [ValidateScript({(Get-ADComputer -Identity $_).objectclass -eq 'computer' })]
                   [String]$Hostname,
        # Param3 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=2,
                   ParameterSetName='Local Host')]
                   [Switch]$RunOnLocalHost,
        # Param4 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipeline=$true,
                   Position=3)]
                   [ValidateSet("Shutdown","Startup")]
                   [String]$Logic
    )

    Begin
    {
        
        $path = (get-item -Path .).FullName 
        
        if ($RunOnLocalHost){
            $localhostname = $env:computername
            $csv = [PSCustomObject]@{
                Hostname = $localhostname}
        }
        elseif ($CSVFile -ne $null){
            Write-Host "Importing $CSVFile..."
            $csv = import-csv "$CSVFile"
        }else{
            $csv = [PSCustomObject]@{
                Hostname = $Hostname}
        }
        Write-Host "=========================================="
        Write-Host "LAPS Password Expiration Date Reset Tool"
        Write-Host "=========================================="
        Write-Host "v0.5 (2017-12-26) by dmaiolo"
        New-DGMCMTraceLog -message ("Starting Logging for Reset-LAPSPasswordExpiration") -component "Main()" -type 1 
    }
    Process
    {
    
    $computers = @();
        
    $csv | foreach-object {
        $h = $_.Hostname
    

        if(Test-Connection -ComputerName $h -Count 1 -Quiet){
            
            $comp = Get-ADComputer $h -Properties ms-MCS-AdmPwdExpirationTime
            
            try{
                $currentexpirationtime = $([datetime]::FromFileTime([convert]::ToInt64($comp.'ms-MCS-AdmPwdExpirationTime',10)))
                New-DGMCMTraceLog -message ("$h`: The current LAPS password expiration is $currentexpirationtime.") -component "Main()" -type 1
            }catch{
                New-DGMCMTraceLog -message ("$h`: The current LAPS password has an unknown expiration or is already clear.") -component "Main()" -type 2
            }

            Write-Host "$h`: Resetting ms-MCS-AdmPwdExpirationTime..."
            try{
                Set-ADComputer $h -Clear "ms-MCS-AdmPwdExpirationTime"
                New-DGMCMTraceLog -message ("$h`: ms-MCS-AdmPwdExpirationTime has been cleared succesfully.") -component "Main()" -type 1
            }catch{
                New-DGMCMTraceLog -message ("$h`: ms-MCS-AdmPwdExpirationTime could not be cleared.") -component "Main()" -type 3
            }
            if ($Logic -eq "Startup"){
                New-DGMCMTraceLog -message ("$h`: Startup Sequence Logic Was Initiated.") -component "Main()" -type 1
                Write-Host "$h`: Sleeping 3 seconds..."
                sleep 3

                Write-Host "$h`: Running GPUpdate /Force on $h ..."
                if ($RunOnLocalHost){
                    try{
                        $command = "GPUPdate /Target:Computer /Force"
                        Invoke-Expression -Command:"$command"
                        New-DGMCMTraceLog -message ("$h`: Group Policy Was Succesfully Updated.") -component "Main()" -type 1
                    }catch{
                        New-DGMCMTraceLog -message ("$h`: Group Policy Could Not Update.") -component "Main()" -type 3
                    }
                }else{
                    try{
                        Invoke-GPUpdate -Computer $h -Force
                        New-DGMCMTraceLog -message ("$h`: Group Policy Was Succesfully Updated.") -component "Main()" -type 1
                    }catch{
                        New-DGMCMTraceLog -message ("$h`: Group Policy Could Not Update.") -component "Main()" -type 3
                    }
                }
            }elseif ($Logic -eq "Shutdown"){
                New-DGMCMTraceLog -message ("$h`: Shutdown Sequence Logic Was Initiated. Skipping GPUpdate") -component "Main()" -type 1
            }
            Write-Host "$h`: Sleeping 5 Seconds..."
            sleep 5
            try{
                $currentexpirationtime = $([datetime]::FromFileTime([convert]::ToInt64($comp.'ms-MCS-AdmPwdExpirationTime',10)))
                New-DGMCMTraceLog -message ("$h`: The updated LAPS password expiration is $currentexpirationtime.") -component "Main()" -type 1
            }catch{
                New-DGMCMTraceLog -message ("$h`: The updated LAPS password has an unknown expiration.") -component "Main()" -type 3
            }
        }
        else{
            New-DGMCMTraceLog -message ("$h`: is offline.") -component "Main()" -type 2
        }
       }
    }
    End
    {
       Write-Host "==============================================================="
       Write-Host "Log File of Results Generated at $Global:LogFile VIEW WITH CMTRACE.EXE"
       New-DGMCMTraceLog -message ("Ending Logging for Reset-LAPSPasswordExpiration") -component "Main()" -type 1
    }
}

$VerboseLogging = "true"
[bool]$Global:Verbose = [System.Convert]::ToBoolean($VerboseLogging)
#$Global:LogFile = Join-Path (GetScriptDirectory) "Reset-LAPSPasswordExpiration_$(Get-Date -Format dd-MM-yyyy).log"
$Global:LogFile = "\\vendscr001prd\Scripts\Reset-LAPSPasswordExpiration\Reset-LAPSPasswordExpiration.log"
$Global:MaxLogSizeInKB = 10240
$Global:ScriptName = 'Reset-LAPSPasswordExpiration.ps1' 
$Global:ScriptStatus = 'Success'