30 May 2017

Find Maximum Possible Resolution for Each Monitor

I have been working on a way that I can ensure the maximum resolution is set on monitors. Every so often, a monitor does not have the resolution set to maximum. I have been trying to figure out a way to set the resolution to maximum for more than a year, especially when a system is built in the build room and then moved to the user's office to be setup with new monitors. The problem I have had was being able to get the maximum resolution value. It is not stored in the system that is easily accessible through PowerShell. I had thought about having the script go through the keystroke process of the Display screen to set the monitor resolution to maximum and then write the resolution values to a text file. PowerShell can retrieve the resolution monitors are currently set to. Finally, it occurred to me the maximum resolution should be stored in the INF driver file. I opened up the file and there it was.

Andy Schneider has this awesome script that can set the resolution of the monitors. The only part was needed were the resolution values. The script below can be used with Andy's to set the resolution to maximum for each installed monitor. The Get-MaximumResolution function returns an array of objects containing the model, horizontal, and vertical resolutions.

Here is the output of the script after it was executed on my machine that has three monitors.



I also want to point out that SAPIEN's PowerShell Studio made writing the script a breeze. It simplifies the process of this and allows for much more thorough scripting.

The script is available to download from my GitHub site located here.


 <#  
      .SYNOPSIS  
           Get Maximum Monitor Resolution  
        
      .DESCRIPTION  
           This script will retrieve the maximum possible resolution for monitors by identifying the associated driver. The driver INF file contains the maximum defined resolution for a monitor. This script is designed for Dell monitors only. It has not been tested on any other brand. Also, the monitors need to be installed in the device manager to get the correct association.   
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.139  
           Created on:       5/30/2017 12:37 PM  
           Created by:       Mick Pletcher  
           Organization:  
           Filename:         MaxResolution.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param ()  
   
 function Get-MaximumResolution {  
      #Create monitor array  
      $Monitors = @()  
      #Get associate monitor hardware ID for each monitor  
      $HardwareIDs = (Get-WmiObject Win32_PNPEntity | where-object { $_.PNPClass -eq "Monitor" }).HardwareID | ForEach-Object { $_.Split("\")[1] }  
      foreach ($Monitor in $HardwareIDs) {  
           #Create object  
           $Object = New-Object -TypeName System.Management.Automation.PSObject  
           #Get the location of the associated driver file  
           $DriverFile = Get-ChildItem -path c:\windows\system32\driverstore -Filter *.inf -recurse | Where-Object { (Select-String -InputObject $_ -Pattern $Monitor -quiet) -eq $true }  
           #Retrieve the maximum resolution from the INF file  
           $MaxResolution = ((Get-Content -Path $DriverFile.FullName | Where-Object { $_ -like "*,,MaxResolution,,*" }).split('"')[1]).Split(",")  
           #Write the Model to the object  
           $Object | Add-Member -MemberType NoteProperty -Name Model -Value $DriverFile.BaseName.ToUpper()  
           #Write the horizontal maximum resolution to the object  
           $Object | Add-Member -MemberType NoteProperty -Name "Horizontal(X)" -Value $MaxResolution[0]  
           #Write the vertical maximum resolution to the object  
           $Object | Add-Member -MemberType NoteProperty -Name "Vertical(Y)" -Value $MaxResolution[1]  
           #Write the object to the array  
           $Monitors += $Object  
      }  
      Return $Monitors  
 }  
   
 #Display list of monitor with maximum available resolutions  
 $Monitors = Get-MaximumResolution  
 $Monitors  
   

23 May 2017

Pending Reboot Reporting with Orchestrator

As we are implementing the ADR in SCCM for servers, we want to know if systems are pending a reboot without having to log into every server. Thankfully, Kent Agerlund, formulated and posted this awesome solution for tracking pending reboots using a compliance rule and baseline in SCCM. It reports systems that are waiting for a reboot to a non-compliance collection.

I wanted to take this to the next level with automated reporting via email. I wrote the script below that queries the non-compliance collection in SCCM and writes the results to a .CSV file and emails that file to the appropriate IT staff. I integrated this with Orchestrator so this process becomes an automated process.

To implement this in Orchestrator, you will need to use the monitor date/time activity to schedule the execution. I have it scheduled for every day.



Next, you link a Run Program activity to run the PowerShell script.



The next activity is the Get File Status. This will check to see if the .CSV file exists. The PowerShell script will delete the old .CSV file and will not create a new one if there are no systems pending a reboot.



The next thing is to customize the link between the Get File Status and Send Email activities. This stops the Send Email from taking place if the .CSV file is not present.


Finally, the Send Email activity is executed to send an email to the appropriate IT staff with the attached .CSV file.


Here is a screenshot of my runbook.

I have included in the script an example of how to implement this in the command line. You can download the PowerShell script from my GitHub site located here.

I also wanted to say how much easier SAPIEN's PowerShell Studio made writing this script. PowerShell studio is a fantastic PowerShell editing tool that takes coding to a whole new level.

 <#  
      .SYNOPSIS  
           Reboot Report  
        
      .DESCRIPTION  
           This script will query SCCM for a list of machines pending a reboot. It will then write the list to a .CSV file.  
        
      .PARAMETER CollectionName  
           Name of the collection to query for a list of machines  
        
      .PARAMETER SCCMServer  
           Name of the SCCM Server  
        
      .PARAMETER SCCMDrive  
           Drive of the SCCM server  
        
      .PARAMETER ReportFile  
           Name of the file to write the list of systems pending a reboot.  
        
      .EXAMPLE  
           powershell.exe -file RebootReporting.ps1 -CollectionName "All Servers" -SCCMServer ACMESCCM -SCCMDrive CMG -ReportFile "PendingRebootReport.csv"  
   
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.139  
           Created on:       5/22/2017 2:42 PM  
           Created by:       Mick Pletcher  
           Organization:  
           Filename:         RebootReporting.ps1  
           ===========================================================================  
 #>  
 [CmdletBinding()]  
 param  
 (  
      [ValidateNotNullOrEmpty()][string]$CollectionName,  
      [ValidateNotNullOrEmpty()][string]$SCCMServer,  
      [ValidateNotNullOrEmpty()][string]$SCCMDrive,  
      [ValidateNotNullOrEmpty()][string]$ReportFile  
 )  
   
 function Import-SCCMModule {  
 <#  
      .SYNOPSIS  
           Import SCCM Module  
        
      .DESCRIPTION  
           Locate the ConfigurationManager.psd1 file and import it.  
        
      .PARAMETER SCCMServer  
           Name of the SCCM server to connect to.  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()][string]$SCCMServer  
      )  
        
      #Get the architecture of the specified SCCM server  
      $Architecture = (get-wmiobject win32_operatingsystem -computername $SCCMServer).OSArchitecture  
      #Get list of installed applications  
      $Uninstall = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      If ($Architecture -eq "64-bit") {  
           $Uninstall += Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      }  
      #Get the registry key that specifies the location of the SCCM installation drive and directory  
      $RegKey = ($Uninstall | Where-Object { $_ -like "*SMS Primary Site*" }) -replace 'HKEY_LOCAL_MACHINE', 'HKLM:'  
      $Reg = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ItemProperty -Path $args[0] } -ArgumentList $RegKey  
      #Parse the directory listing  
      $Directory = (($Reg.UninstallString).Split("\", 4) | Select-Object -Index 0, 1, 2) -join "\"  
      #Locate the location of the SCCM module  
      $Module = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path $args[0] -Filter "ConfigurationManager.psd1" -Recurse } -ArgumentList $Directory  
      #If more than one module is present, use the latest one  
      If ($Module.Length -gt 1) {  
           foreach ($Item in $Module) {  
                If (($NewModule -eq $null) -or ($Item.CreationTime -gt $NewModule.CreationTime)) {  
                     $NewModule = $Item  
                }  
           }  
           $Module = $NewModule  
      }  
      #format the $Module unc path  
      [string]$Module = "\\" + $SCCMServer + "\" + ($Module.Fullname -replace ":", "$")  
      #Import the SCCM module  
      Import-Module -Name $Module  
 }  
   
 function Get-RebootPendingSystems {  
 <#  
      .SYNOPSIS  
           Reboot Pending Systems  
        
      .DESCRIPTION  
           This function connects to SCCM and retrieves the list of systems pending a reboot.  
        
      .EXAMPLE  
                     PS C:\> Get-RebootPendingSystems  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      #Create Report array  
      $Report = @()  
      #If the SCCM drive does not have a colon at the end, add it  
      If ($SCCMDrive[$SCCMDrive.Length - 1] -ne ":") {  
           $SCCMDrive = $SCCMDrive + ":"  
      }  
      #Change the location to the SCCM drive  
      Set-Location $SCCMDrive  
      #Get list of systems in the SCCM collection that are pending a reboot  
      $Systems = (Get-CMDevice -collectionname $CollectionName).Name | Sort-object  
      foreach ($System in $Systems) {  
           $Object = New-Object -TypeName System.Management.Automation.PSObject  
           $Object | Add-Member -MemberType NoteProperty -Name ComputerName -Value $System.ToUpper()  
           $Report += $Object  
      }  
      #Change location back to the system homedrive  
      Set-Location $env:HOMEDRIVE  
      #Return the list of systems  
      Return $Report  
 }  
   
 function Get-RelativePath {  
 <#  
      .SYNOPSIS  
           Get the relative path  
        
      .DESCRIPTION  
           Returns the location of the currently running PowerShell script  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([string])]  
      param ()  
        
      $Path = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
      Return $Path  
 }  
   
 Clear-Host  
 #Find and import the PowerShell SCCM Module  
 Import-SCCMModule -SCCMServer $SCCMServer  
 #Get a list of systems pending a reboot  
 $Report = Get-RebootPendingSystems  
 #Get the path this script is being executed from  
 $RelativePath = Get-RelativePath  
 #Add the relative path to the filename  
 $ReportFile = $RelativePath + $ReportFile  
 #Delete Report File if it exists  
 If ((Test-Path $ReportFile) -eq $true) {  
      Remove-Item -Path $ReportFile -Force  
 }  
 If (($Report -ne $null) -and ($Report -ne "")) {  
      #Display the list of systems to the screen  
      $Report  
      #Export the list of systems to a CSV file  
      $Report | Export-Csv -Path $ReportFile -Encoding UTF8 -Force -NoTypeInformation  
 }  
   

19 May 2017

Local Administrators Automated Reporting Tool

Back in November 2016, I posted the blog entry on reporting local administrators on machines. That script is deployed to machines via an SCCM package that reports the local administrators back to SCCM to be able to be queried into a report.

I got tired of having to go into SCCM and look at the query report to see if any new machines might have appeared. I decided to write this script that would automate the process. This script is designed to be used with System Center Orchestrator or it can be setup as a scheduled task. It is written to perform a query in SCCM to display the data the above-listed script reported to SCCM to the screen and to a CSV file.

I have set mine up in Orchestrator. Here is the process I used in setting mine up:

The first is setup to run this every day. The second points to the Daily Schedule properties defining which days to run it on. The third is the run program defining to run the PowerShell script. Here is how it is laid out:


The fourth one is defining to send an email with the CSV attachment. Here is how it is setup:


I wanted to take a moment and say how much SAPIEN PowerShell Studio made a difference with writing this code. It made the process so much easier and more efficient.

You can download the script from GitHub.


 <#  
      .SYNOPSIS  
           Execute SCCM Stored Queries  
        
      .DESCRIPTION  
           This script will execute SCCM stored queries.  
        
      .PARAMETER ListQueries  
           Generate a list of queries  
        
      .PARAMETER Query  
           Name of the query to execute  
        
      .PARAMETER SCCMServer  
           Name of SCCM server  
        
      .PARAMETER SCCMServerDrive  
           A description of the SCCMServerDrive parameter.  
        
      .NOTES  
           ===========================================================================  
           Created with:     SAPIEN Technologies, Inc., PowerShell Studio 2017 v5.4.139  
           Created on:       5/8/2017 1:32 PM  
           Created by:       Mick Pletcher  
           Filename:         SCCMQuery.ps1  
           ===========================================================================  
 #>  
 param  
 (  
      [switch]$ListQueries,  
      [string]$Query,  
      [string]$SCCMServer,  
      [string]$SCCMServerDrive  
 )  
   
 function Get-ListOfQueries {  
 <#  
      .SYNOPSIS  
           Get List of Queries  
        
      .DESCRIPTION  
           This function will retrieve a list of all queries in SCCM  
        
      .EXAMPLE  
                     PS C:\> Get-ListOfQueries  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      Set-Location $SCCMServerDrive  
      $Queries = Get-CMQuery  
      Set-Location $env:SystemDrive  
      $QueryArray = @()  
      foreach ($Query in $Queries) {  
           $QueryArray += $Query.Name  
      }  
      $QueryArray = $QueryArray | Sort-Object  
      $QueryArray  
 }  
   
 function Get-RelativePath {  
 <#  
      .SYNOPSIS  
           Get the relative path  
        
      .DESCRIPTION  
           Returns the location of the currently running PowerShell script  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()][OutputType([string])]  
      param ()  
        
      $Path = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent) + "\"  
      Return $Path  
 }  
   
 function Get-SCCMQueryData {  
      [CmdletBinding()]  
      param ()  
        
      $Report = @()  
      #Change directory to the SCCM server drive  
      Set-Location $SCCMServerDrive  
      #Retrieve report from SCCM  
      $Output = Get-CMQuery -Name $Query | Invoke-CMQuery  
      #Change directory back to the system this script is running on  
      Set-Location $env:SystemDrive  
      #Parse through data and create report object  
      foreach ($Item in $Output) {  
           $Item1 = [string]$Item  
           $Domain = (($Item1.split(';'))[0]).Split('"')[1]  
           $User = ((($Item1.split(";"))[1]).Split('"'))[1]  
           $ComputerName = ((($Item1.split(";"))[3]).Split('"'))[1]  
           $Object = New-Object -TypeName System.Management.Automation.PSObject  
           $Object | Add-Member -MemberType NoteProperty -Name ComputerName -Value $ComputerName.ToUpper()  
           $Object | Add-Member -MemberType NoteProperty -Name Domain -Value $Domain.ToUpper()  
           $Object | Add-Member -MemberType NoteProperty -Name UserName -Value $User.ToUpper()  
           $Report += $Object  
      }  
      $Report = $Report | Sort-Object -Property UserName  
      Return $Report  
 }  
   
 function Import-SCCMModule {  
 <#  
      .SYNOPSIS  
           Import SCCM Module  
        
      .DESCRIPTION  
           Locate the ConfigurationManager.psd1 file and import it.  
        
      .PARAMETER SCCMServer  
           Name of the SCCM server to connect to.  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param  
      (  
           [ValidateNotNullOrEmpty()][string]$SCCMServer  
      )  
        
      #Get the architecture of the specified SCCM server  
      $Architecture = (get-wmiobject win32_operatingsystem -computername $SCCMServer).OSArchitecture  
      #Get list of installed applications  
      $Uninstall = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      If ($Architecture -eq "64-bit") {  
           $Uninstall += Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path REGISTRY::"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" -Force -ErrorAction SilentlyContinue }  
      }  
      #Get the registry key that specifies the location of the SCCM installation drive and directory  
      $RegKey = ($Uninstall | Where-Object { $_ -like "*SMS Primary Site*" }) -replace 'HKEY_LOCAL_MACHINE', 'HKLM:'  
      $Reg = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ItemProperty -Path $args[0] } -ArgumentList $RegKey  
      #Parse the directory listing  
      $Directory = (($Reg.UninstallString).Split("\", 4) | Select-Object -Index 0, 1, 2) -join "\"  
      #Locate the location of the SCCM module  
      $Module = Invoke-Command -ComputerName $SCCMServer -ScriptBlock { Get-ChildItem -Path $args[0] -Filter "ConfigurationManager.psd1" -Recurse } -ArgumentList $Directory  
      #If more than one module is present, use the latest one  
      If ($Module.Length -gt 1) {  
           foreach ($Item in $Module) {  
                If (($NewModule -eq $null) -or ($Item.CreationTime -gt $NewModule.CreationTime)) {  
                     $NewModule = $Item  
                }  
           }  
           $Module = $NewModule  
      }  
      #format the $Module unc path  
      [string]$Module = "\\" + $SCCMServer + "\" + ($Module.Fullname -replace ":", "$")  
      #Import the SCCM module  
      Import-Module -Name $Module  
 }  
   
 function Send-Report {  
 <#  
      .SYNOPSIS  
           Email report  
        
      .DESCRIPTION  
           A detailed description of the Send-Report function.  
        
      .EXAMPLE  
                     PS C:\> Send-Report  
        
      .NOTES  
           Additional information about the function.  
 #>  
        
      [CmdletBinding()]  
      param ()  
        
      #TODO: Place script here  
 }  
   
 Clear-Host  
 #Add colon to end of SCCMServerDrive if not present  
 If ($SCCMServerDrive[$SCCMServerDrive.Length - 1] -ne ":") {  
      $SCCMServerDrive += ":"  
 }  
 #Import SCCM module  
 Import-SCCMModule -SCCMServer $SCCMServer  
 #Generate a list of all available queries in SCCM  
 If ($ListQueries.IsPresent) {  
      Get-ListOfQueries  
 }  
 #If query is not filled in, then end the script  
 If (($Query -ne $null) -and ($Query -ne "")) {  
      #Perform query from SCCM  
      $Report = Get-SCCMQueryData | Sort-Object -Property ComputerName  
      #Display report to screen  
      $Report  
      #Get path where this script is executing from  
      $RelativePath = Get-RelativePath  
      #Location where to write the report to  
      $File = $RelativePath + "LocalAdministrators.csv"  
      #Delete old report if it exists  
      If ((Test-Path $File) -eq $true) {  
           Remove-Item -Path $File -Force  
      }  
      #Write new report to CSV file  
      $Report | Export-Csv -Path $File -Encoding UTF8 -Force  
 }