Find Workstations That Don't Meet The Company "Standards"
Without standards, there are more variables to manage and the burden of IT can become overbearing.
Brian Scheewe
Monday, October 23, 2023
standard [ stan-derd ] noun
something set up and established by authority as a rule for the measure of quantity, weight, extent, value, or quality
Well-run IT teams have "standards". For example: we use Dell servers. We only install Fortinet firewalls. We use Duo for all MFA. We change passwords semi-annually. We don't store files on end-user devices. We don't make any infrastructure changes on Fridays. We don't install network equipment next to water sources, etc.
Without standards, there are more variables to manage and the burden of IT can become overbearing. When systems are similar, familiar, and predictable, then support costs drop. Hopefully your team has a long list of standards and these standards have become law in your organization.
It gets even better when the business leadership buys-in to the idea of standards. Once they get it, there's no longer a fight every time something needs to be improved. When leaders understand standards, they don't think in terms of "if" a project will be approved, but "when". Helping them understand the spectrum of risks associated with not sticking to standards will help them prioritize what projects to do first.
Some standards are policy driven, and require a human to check (hey this network rack is in the boiler room!). Other standards can be automated so that when a standard is breached a notification or remediation action can be triggered.
End-user devices should meet standards, and most if not all can be automated. Today we'll walk through a PowerShell script that will check for a variety of workstation standards that are commonly established by IT teams.
Windows OS version must be at least X
This will ensure that there aren't legacy unsupported operating systems in your environment.
copy
#Set the minimum OS build number. Windows 10 v1607 is build 14393
$DesktopOSMinimumVersion = 14393
#Get the OS version
$OSVer = [System.Environment]::OSVersion.Version | Select-Object -ExpandProperty Build
#Alert if the OS version is less than $DesktopOSMinimumVersion
if ($OSVer -lt $DesktopOSMinimumVersion) {
Write-Host "ALERT: Windows version is less than $DesktopOSMinimumVersion"
}
Must have less than X pending Windows Updates
If a machine has a growing list of pending Windows updates, then it time to investigate!
copy
#Set the alert threshold for pending updates quantity
$PendingUpdates = 15
#Check available Windows Updates
$UpdateList = ((New-Object -ComObject Microsoft.Update.Session).CreateUpdateSearcher()).Search('IsInstalled=0 and IsHidden=0').updates
$NumberPendingUpdates = $UpdateList.count
if ($NumberPendingUpdates -gt $PendingUpdates) {
Write-Host "ALERT: There are $NumberPendingUpdates pending updates that have not been installed."
}
Windows must be activated
copy
#Check if Windows is activated
$ActivationStatus = Get-CimInstance SoftwareLicensingProduct -Filter "Name like 'Windows%'" | where { $_.PartialProductKey } | select -ExpandProperty LicenseStatus
if ($ActivationStatus = 0) {
Write-Host "ALERT: Windows is not activated."
}
Workstations must have at least X GB of memory
You probably have stacks of recovered RAM sitting around your office. Put it to good use and make sure everyone has equal access to Chrome's many tabs. 😉
copy
#Set the RAM minimum threshold in GB
$RAM_Minimum = 8
#Verify RAM is greater than $RAM_Minimum
$RAM = (Get-WmiObject Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum | select -ExpandProperty Sum) / 1GB
if ($RAM -lt $RAM_Minimum) {
Write-Host "ALERT: PC has too little RAM: $RAM GB."
}
CPU must be at least X generation
One of the best indicators of a machine's age is the CPU. But aside from age, we also want to ensure that company devices are performant. Taking in CPU generation and model will ensure that no one has a slow machine.
copy
# Choose the minimum CPU generations
$MinimumIntelGeneration = 7 #7th gen Intel core CPUs
$MinimumAmdGeneration = 2000 #2000 series of Ryzen
#Get the CPU make and model
$cpuInfo = Get-WmiObject Win32_Processor | Select-Object -ExpandProperty Name
$CPUManufacturer = Get-WmiObject Win32_Processor | Select-Object -ExpandProperty Manufacturer
# Regular expression to extract specifically the model number
$regex = [regex]::Match($cpuInfo, 'i\d{1,2}-\d{1,5}|Pentium|Atom|i3|E-2\d{3}|E3-\d{4}|E5-\d{4}|\d{4} |\d{4}.| \d{3} ')
if ($regex.Success) {
$cpuModel = $regex.Value
}
else {
Write-Host "ALERT: Can't determine the CPU model/generation."
}
# Intel CPU check.
if ($CPUManufacturer -like "GenuineIntel") {
# Check for Core generation and model
if ($cpuModel -match 'i\d{1,2}-\d{1,5}') {
$generationMatch = $cpuModel -split '-' | Select-Object -Last 1
$generation = $generationMatch -replace '\D' # Extract and convert digits only
#If model number is 5 digits long, then the first 2 digits make up the generation. i7-12700 is 12th gen
if ($generation.Length -eq 5) {
$generation = $generation.Substring(0, 2)
}
#If the model is 4 digits long, then the first digit is the generation. i5-7500 is 7th gen.
else {
$generation = $generation[0]
}
#This is where we check the Intel generation against the standard
if ([int]$generation -lt $MinimumIntelGeneration) {
Write-Host "ALERT: The CPU generation is too old."
}
}
elseif ($cpuModel -match 'E5-\d{4}') {
#Xeon E5 generation. Eventually move this into the elseif below
}
#Alert on low-powered CPUs
elseif ($cpuModel -match 'i3|Pentium|Atom|E3-\d{4}| \d{3} ') {
Write-Host "ALERT: CPU model type does not meet standards. (i3, Pentium, Xeon E3, etc)"
}
}
# AMD CPU Check
elseif ($CPUManufacturer -like "AuthenticAMD") {
# Ryzen is the standard in this case for AMD
if ($cpuInfo -match 'Ryzen') {
# Remove non-numeric characters and convert to int
$generationMatch = $cpuModel -replace '[^\d]', ''
$generation = [int]$generationMatch
#Compare the CPU to the standard
if ($generation -lt $MinimumAmdGeneration) {
Write-Host "ALERT: The AMD CPU generation is too old."
}
}
else {
Write-Host "ALERT: CPU model type does not meet standards for AMD CPUs."
}
}
else {
Write-Host "ALERT: Not an Intel or AMD CPU"
}
Files are saved on a server and not locally
In order to ensure all data is centralized, governed, and backed-up, it must be on the server. If we see files piling up in the users' My Docs folder, then that's a great indicator that someone has a bad habit.
copy
#The file count threshold before generating an alert
$FileLimitInMyDocs = 30
# Count the number of files in the My Docs folders and alert if they exceed a threshold ($FileLimitInMyDocs)
$UserFolderList = Get-ChildItem c:\users\ | Where-Object { $_.Name -notin @("Public", "administrator") }
foreach ($User in $UserFolderList.Name) {
$MyDocs = "c:\users\$User\Documents"
$FileCount = (Get-ChildItem -Recurse -File $MyDocs | Measure-Object).Count
if ($FileCount -gt $FileLimitInMyDocs) {
Write-Host "ALERT: $User appears to be saving $FileCount files in their local My Docs folder, and not on the server."
}
}
No spinning disks, only SSDs
copy
# Check if the disk is an SSD
$DiskType = Get-PhysicalDisk | Where-Object { $_.DeviceID -eq "0" } | select -ExpandProperty MediaType
if ($DiskType -notlike "SSD") {
Write-Host "ALERT: The primary disk is not an SSD and is instead a $DiskType"
}
Must be joined to the Active Directory domain
copy
#Check if joined to a domain
$DomainJoined = (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain
if (! $DomainJoined) {
Write-Host "ALERT: Not joined to a domain."
}
Screen/display resolution must be X resolution or higher
Sometimes monitors get handed down to the new guy/gal. This will ensure that even the newbie isn't stuck with something that cramps productivity.
copy
#Set the minimum screen resolution
$MinimumHorizontalResolution = 1920
$MinimumVerticalResolution = 1080
#Check the monitor (display) resolution isn't lower than $MinimumHorizontalResolution or $MinimumVerticalResolution
$monitorInfo = Get-CimInstance Win32_VideoController
$CurrentVerticalResolution = $monitorInfo.CurrentVerticalResolution
$CurrentHorizontalResolution = $monitorInfo.CurrentHorizontalResolution
$VerticalResolution = [int]::MaxValue
$HorizontalResolution = [int]::MaxValue
#If the $monitor.CurrentXResolution is blank, then use the advertised resolution from $monitorInfo.VideoModeDescription. This can happen if the monitor goes to sleep.
if (($null -eq $CurrentVerticalResolution) -or ($null -eq $CurrentHorizontalResolution )) {
$videoModeDescription = $monitorInfo.VideoModeDescription
$resolutions = $videoModeDescription | Select-String -Pattern '(\d+) x (\d+)'
if ($resolutions) {
$resolutionMatches = $resolutions.Matches[0].Groups
$CurrentHorizontalResolution = [int]$resolutionMatches[1].Value
$CurrentVerticalResolution = [int]$resolutionMatches[2].Value
}
}
else {
# Otherwise, if there is more than one monitor, then get the lowest resolution of them all.
foreach ($monitor in $monitorInfo) {
if ($monitor.CurrentVerticalResolution -lt $verticalResolution) {
$verticalResolution = $monitor.CurrentVerticalResolution
}
if ($monitor.CurrentHorizontalResolution -lt $horizontalResolution) {
$horizontalResolution = $monitor.CurrentHorizontalResolution
}
}
}
#Compare the resolution with $MinimumHorizontalResolution and $MinimumVerticalResolution
if (($horizontalResolution -lt $MinimumHorizontalResolution) -or ($verticalResolution -lt $MinimumVerticalResolution) ) {
Write-Host "ALERT: Screen resolution is ($horizontalResolution x $verticalResolution) which is less than ($MinimumHorizontalResolution x $MinimumVerticalResolution)"
}
Microsoft Outlook is at least X version
Yes this specifically checks Outlook, but this alone is a good clue about the age of all the Office apps. At the time of writing, Outlook 2013 is end-of support, so that's an easy line to draw in the sand.
copy
#Chose the minimum allowed version
$MinimumOutlookVersion = 16 #Outlook 2016, 2019, Outlook 365 and above show as version 16
#Check that the Outlook version installed isn't lower than $MinimumOutlookVersion.
$OutlookRegistryData = reg query "HKEY_CLASSES_ROOT\Outlook.Application\CurVer" 2>$null # Outlook 2016, 2019, Outlook 365 and above show as version 16 :shrug:
#If the registry value is empty, then Outlook is not installed.
if ($null -ne $OutlookRegistryData) {
[int]$OutlookVersion = [regex]::Match($OutlookRegistryData, '\.(\d+)').Groups[1].Value
if ($OutlookVersion -lt $MinimumOutlookVersion ) {
Write-Host "ALERT: Outlook version ($OutlookVersion) is less than or equal to version $MinimumOutlookVersion"
}
}
else {
write-host "Outlook not installed. Moving on..."
}
Putting it all together
There's no reason why all these checks can't be put into a single script, and we've done just that here: Audit Workstation Standards
Additionally, this combined version will tally up the alerts and send a helpful output to the console. The easiest way to run this on all devices is to use Level! In the Level app go to Scripts add a new script and paste the script from GitHub.
This script can either be run on demand (for a (v)CIO audit of workstations), or it can be setup as a monitor for continuous checking. If using this for (v)CIO purposes to create workstation replacement schedules, export the script results by clicking "Download Results" at the top right. This will provide a CSV that can be used for finer reporting.
At Level, we understand the modern challenges faced by IT professionals. That's why we've crafted a robust, browser-based Remote Monitoring and Management (RMM) platform that's as flexible as it is secure. Whether your team operates on Windows, Mac, or Linux, Level equips you with the tools to manage, monitor, and control your company's devices seamlessly from anywhere. Ready to revolutionize how your IT team works? Experience the power of managing a thousand devices as effortlessly as one. Start with Level today—sign up for a free trial or book a demo to see Level in action.