Compare Driver Versions and Hotfixes

Show running drivers with different versions on two different systems and also show drivers that exist on one but not the other.
Also compares OS hotfixes between the two systems and shows any differences.
The account that runs the script must have remote WMI/CIM permission to the other machine.
Version 1.1.9
Created on 2024-02-01
Modified on 2024-02-25
Created by Guy Leech
Downloads: 23

The Script Copy Script Copied to clipboard
#requires -version 3

<#
.SYNOPSIS
    Show driver differences between 2 machines
    
.DESCRIPTION
    Account running the script must have remote WMI/CIM access to the toher machine.

.PARAMETER otherMachine
    The name of the other machine to compare with this one

.PARAMETER runningOnly
    Whether to compare all drivers or jsut those currently running on the machine where the script is run

.NOTES
    Modification History:

    2024/01/30  @guyrleech  Script born
    2024/02/23  @guyrleech  Added help
#>

[CmdletBinding()]

Param
(
    [Parameter(Mandatory=$true)]
    [string]$otherMachine ,
    [ValidateSet('yes','no','true','false')]
    [string]$runningOnly = 'yes'
)

$VerbosePreference = $(if( $PSBoundParameters[ 'verbose' ] ) { $VerbosePreference } else { 'SilentlyContinue' })
$DebugPreference = $(if( $PSBoundParameters[ 'debug' ] ) { $DebugPreference } else { 'SilentlyContinue' })
$ErrorActionPreference = $(if( $PSBoundParameters[ 'erroraction' ] ) { $ErrorActionPreference } else { 'Stop' })
$ProgressPreference = 'SilentlyContinue'

[int]$outputWidth = 400
try
{
    if( ( $PSWindow = (Get-Host).UI.RawUI ) -and ( $WideDimensions = $PSWindow.BufferSize ) )
    {
        $WideDimensions.Width = $outputWidth
        $PSWindow.BufferSize = $WideDimensions
    }
}
catch
{
    ## not a showstopper
}

if( $otherMachine -ieq $env:COMPUTERNAME )
{
    Throw "Both machines specified are $otherMachine"
}

## Win32_PnPSignedDriver gives us version info, win32_systemdriver does not but does give us full path to driver file

$remoteSessionOptions = New-CimSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$remoteSession = $null

try
{
    $remoteSession = New-CimSession -ComputerName $otherMachine -SessionOption $remoteSessionOptions -ErrorAction Continue
    if( $null -eq $remoteSession )
    {
        Throw "Failed to create remote CIM session to $otherMachine"
    }

    $localOS = Get-CimInstance -ClassName Win32_OperatingSystem
    $remoteOS = $null
    $remoteOS = Get-CimInstance -ClassName Win32_OperatingSystem -CimSession $remoteSession

    [array]$localSystemDrivers  = @( Get-CimInstance -ClassName Win32_SystemDriver )
    [array]$remoteSystemDrivers = @( Get-CimInstance -ClassName Win32_SystemDriver -CimSession $remoteSession)
    $notFoundRemotely = New-Object -TypeName System.Collections.Generic.List[object]

    Write-Verbose -Message "$($localSystemDrivers.Count) local system drivers, $($remoteSystemDrivers.Count) remote"

    [array]$differences = @( ForEach( $driver in $localSystemDrivers ) ## this gives us driver file name so we can look at actual versions which we need to do reagardless of what driver info says as that comes from inf file and cannot be trusted
    {
        if( ( $runningOnly -match 'yes|true' -and $driver.State -ieq 'Running' ) -or $runningOnly -match 'no|false' )
        {
            $localFileDetails = Get-ItemProperty -Path ([Environment]::ExpandEnvironmentVariables( $driver.PathName ) -replace '^\\\?\?\\' ) | Select-Object -ExpandProperty VersionInfo
            $remoteMatches = $remoteSystemDrivers | Where-Object { $_.Name -eq $driver.Name } ## bowser is one that has different description on Server 2016!
            if( $null -eq $remoteMatches -or $remoteMatches.Count -eq 0 )
            {
                $notFoundRemotely.Add( (Add-Member -InputObject $driver -MemberType NoteProperty -Name FileDetails -Value $localFileDetails -PassThru ) )
            }
            else ## some matching drivers present
            {
                if( $remoteMatches -and $remoteMatches -is [array] -and $remoteMatches.Count -gt 1 )
                {
                    ## TODO check if same version but if more than 1, how do we differentiate and do we need to look for other local drivers matching and process them all here and not check the similar ones again later ?
                    ## will get multiple matches for things like processors
                    Write-Verbose -Message "$($driver.Description): $($driver.FriendlyName) has $($remoteMatches.Count) matches"
                }
                else ## only 1 matching driver
                {   
                    $remoteFileDetails = $null
                    ## some paths have \\??\ at the start so strip that and escape \ with extra \
                    $remoteFileDetails = Get-CimInstance -ClassName CIM_DataFile -Filter "Name = '$([Environment]::ExpandEnvironmentVariables( $driver.PathName ) -replace '^\\\?\?\\' -replace '\\' , '\\')'" -CimSession $remoteSession -Verbose:$false
                    if( $localFileDetails.FileVersionRaw.ToString() -ne $remoteFileDetails.Version )
                    {
                        Write-Verbose -Message "** $($driver.Caption) has different version numbers **"
                        $result = [pscustomobject]@{
                            Driver = $driver.Caption
                            File = $driver.PathName
                            'Version' = $localFileDetails.FileVersionRaw
                            'Remote Version' = $remoteFileDetails.Version
                            'Local Newer' = $localFileDetails.FileVersionRaw -gt [version]$remoteFileDetails.Version
                        }
                        if( $runningOnly -notmatch 'yes|true' )
                        {
                            Add-Member -InputObject $result -MemberType NoteProperty -Name State -Value $driver.State
                        }
                        Add-Member -InputObject $result -MemberType NoteProperty -Name 'Remote State' -Value $remoteMatches.State
                        $result ## output
                    }
                }
            }
        }
        else
        {
            Write-Verbose -Message "Ignoring `"$($driver.DisplayName)`" as not running"
        }
    })

    Write-Verbose -Message "Got $($notFoundRemotely.count) drivers out of $($localSystemDrivers.Count) not found on $otherMachine ($($remoteSystemDrivers.Count) drivers), $($differences.Count) with differences"

    [array]$notFoundLocally = @( ForEach( $remoteDriver in $remoteSystemDrivers )
    {
        ## check present locally
        $localMatches = @( $localSystemDrivers | Where-Object Name -ieq $remoteDriver.Name )
        if( $null -eq $localMatches -or $localMatches.Count -eq 0 )
        {
            ## fetch remote file version info so we have vendor in case not obvious/Microsoft
            $remoteFileDetails = $null
            ## environment variables expanded locally not on remote system but most likely are the same
            $remoteFileDetails = Get-CimInstance -ClassName CIM_DataFile -Filter "Name = '$( [Environment]::ExpandEnvironmentVariables( $remoteDriver.PathName ) -replace '^\\\?\?\\' -replace '\\' , '\\')'" -CimSession $remoteSession -Verbose:$false
            Add-Member -InputObject $remoteDriver -MemberType NoteProperty -Name FileDetails -Value $remoteFileDetails -PassThru ## output
        }
    })

    Write-Verbose -Message "Found $($notFoundLocally.Count) drivers which exist remotely but are not present locally"

    if( $localOS.Caption -ine $remoteOS.Caption )
    {
        Write-Warning -Message "Comparing different operating systems : $($localOS.Caption) and $($remoteOS.Caption)"
    }
    elseif( $localOS.BuildNumber -ine $remoteOS.BuildNumber )
    {
        Write-Warning -Message "Comparing different build numbers of $($localOS.Caption) : $($localOS.BuildNumber) and $($remoteOS.BuildNumber)"
    }

    Write-Output -InputObject "Found $($differences.Count) drivers with different driver versions:"
    $differences | Format-Table -AutoSize

    Write-Output -InputObject "Found $($notFoundLocally.Count) drivers on remote system that are not present locally"
    $notFoundLocally | Select-Object -Property Name,ServiceType,Description,@{name='Manufacturer';expression= {$_.FileDetails.Manufacturer}},@{name='Version';expression= {$_.FileDetails.Version}} | Format-Table -AutoSize

    Write-Output -InputObject "Found $($notFoundRemotely.Count) drivers locally that are not present on remote system"
    $notFoundRemotely | Select-Object -Property Name,State,ServiceType,@{name='Description';expression= {$_.FileDetails.FileDescription}},@{name='Manufacturer';expression= {$_.FileDetails.CompanyName}},@{name='Version';expression= {$_.FileDetails.FileVersion}} | Format-Table -AutoSize

    [hashtable]$localHotfixes = @{}
    [hashtable]$remoteHotfixes = @{}
    Get-CimInstance -ClassName Win32_QuickFixEngineering | ForEach-Object `
    {
        try
        {
            $localHotfixes.Add( $_.HotFixId , $_ )
        }
        catch
        {
        $null
        }
    }
    Get-CimInstance -ClassName Win32_QuickFixEngineering -CimSession $remoteSession | ForEach-Object `
    {
        try
        {
            $remoteHotfixes.Add( $_.HotFixID , $_ )
        }
        catch
        {
        $null
        }
    }

    [array]$hotfixOnlyLocally = @( $localHotfixes.GetEnumerator() | Where-Object { -Not $remotehotfixes[ $_.Name ] } )
    [array]$hotfixOnlyRemote  = @( $remoteHotfixes.GetEnumerator() | Where-Object { -Not $localHotfixes[ $_.Name ] } )

    if( $hotfixOnlyLocally.Count -eq 0 -and $hotfixOnlyRemote.Count -eq 0 )
    {
        Write-Output -InputObject "Both systems have the same $($localHotfixes.Count) hotfixes"
    }
    if( $hotfixOnlyLocally.Count -gt 0 )
    {
        Write-Output -InputObject "$($hotfixOnlyLocally.Count) hotfixes out of $($localHotfixes.Count) only on local system:"
        $hotfixOnlyLocally.GetEnumerator() | Select-Object -ExpandProperty value | Sort-Object -Property InstalledOn | Select-Object HotfixId,Description,@{name='Installed';expression={ $_.InstalledOn.ToString( 'd' ) }} | Format-Table -AutoSize
    }
    if( $hotfixOnlyRemote.Count -gt 0 )
    {
        Write-Output -InputObject "$($hotfixOnlyRemote.Count) hotfixes out of $($remoteHotfixes.Count) only on remote system:"
        $hotfixOnlyRemote.GetEnumerator() | Select-Object -ExpandProperty value | Sort-Object -Property InstalledOn | Select-Object HotfixId,Description,@{name='Installed';expression={ $_.InstalledOn.ToString( 'd' ) }} | Format-Table -AutoSize
    }
}
catch
{
    throw
}
finally
{
    if( $remoteSession )
    {
        Remove-CimSession -CimSession $remoteSession
        $remoteSession = $null
    }
}