Get HP Driver Pack Info with PowerShell – Web Scraping Method

So I was preparing an OSD task sequence in ConfigMgr to deploy Windows 10 1909 and I wanted to know if any of the HP workstations I would be deploying to had updated driver packs available for 1909, since I had simply copied the task sequence used for 1903.

A while ago I posted a blog with a script to download the latest driver packs from Dell using a web-scraping method, so I decided to take a similar approach for HP driver packs. HP publish a list of the driver packs available for various models and OS versions on the web in a tabular format, so I decided to try to convert that HTML table into data that could also be displayed in table format in PowerShell as well as being queryable, and the script below is the result.

[CmdletBinding()]
Param
(
[Parameter(Mandatory=$false)]
[ValidateSet('64-bit','32-bit')]
$Architecture = "64-bit"
)
# Load the HP driver pack web page
If ($Architecture -eq "64-bit")
{
$URI = "https://ftp.hp.com/pub/caps-softpaq/cmit/HP_Driverpack_Matrix_x64.html"
}
If ($Architecture -eq "32-bit")
{
$URI = "https://ftp.hp.com/pub/caps-softpaq/cmit/HP_Driverpack_Matrix_x86.html"
}
$HTML = Invoke-WebRequest Uri $URI ErrorAction Stop
# Scrape the table rows and headers for the datatable
$TableRows = $HTML.AllElements | where {$_.tagName -eq "TR"-and $_.innerHTML -match "<P>"}
$Headers = ($HTML.AllElements | where {$_.tagName -eq "TR" -and $_.innerHTML -match "Windows 10"})[0].innerHTML
[string]$ColumnExpression = "<td[^>]*>(.*?)</td>"
$HeaderMatches = [Regex]::Matches($Headers, $ColumnExpression, ([System.Text.RegularExpressions.RegexOptions]::Multiline, [System.Text.RegularExpressions.RegexOptions]::Singleline, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase))
# Load the headers as columns into a datatable
$DataTable = New-Object System.Data.DataTable
[void]$DataTable.Columns.Add("Model")
foreach ($Match in ($HeaderMatches | Select Skip 1))
{
[void]$DataTable.Columns.Add($Match.Value.Split('>')[1].Replace('</TD',''))
}
foreach ($Table in $TableRows)
{
[string]$ColumnExpression = "<td[^>]*>(.*?)</td>"
$Matches = [Regex]::Matches($Table.innerHTML, $ColumnExpression, ([System.Text.RegularExpressions.RegexOptions]::Multiline, [System.Text.RegularExpressions.RegexOptions]::Singleline, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase))
$array = New-Object System.Collections.ArrayList
# Process each regex match (table data entry)
foreach ($Item in $Matches)
{
# Add the models
If ($Item.Index -eq 0)
{
[array]$Models = ($Item.Value.Split('>') | where {$_ -match "/p"}).Replace("</P","")
foreach ($Model in $Models)
{
[string]$ModelString = $ModelString + "$Model, "
}
$ModelString = $ModelString.TrimEnd(', ')
[void]$array.Add($ModelString)
Remove-Variable Name ModelString Force
}
# Add the driver pack url and version
Else
{
If ($Item.Value -notmatch "<P>")
{
$Value = ""
}
Else
{
$href = $Item.Value.Split() | Where {$_ -match "href"} | Where {$_ -match "exe"}
$Link = $href.Split('>')[0].Replace('href="','').TrimEnd('"')
$Version = ($Item.Value.Split('=') | where {$_ -match "Version"}).Replace('"','').TrimEnd(' href')
$Value = "$Link ($Version)"
}
[void]$array.Add($Value)
}
}
# Add an entry to the datatable
If ($Architecture -eq '64-bit')
{
[void]$DataTable.Rows.Add($array[0],$array[1],$array[2],$array[3],$array[4],$array[5],$array[6],$array[7],$array[8],$array[9],$array[10],$array[11])
}
If ($Architecture -eq '32-bit')
{
[void]$DataTable.Rows.Add($array[0],$array[1],$array[2],$array[3],$array[4],$array[5],$array[6])
}
}
Return $DataTable

This kind of data displays well in PowerShell’s gridview, like so:

Get-HPDriverPacks | Out-GridView 

In the first column you find the models and the next columns contain the driver pack version, release date and download URL for the various OS versions. You can then use the gridview’s native filtering capabilities to find something specific:

By default, the script will get the 64-bit driver packs, but you can also get 32-bit for Windows 7 etc (you’re not still using Windows 7 are you?!):

Get-HPDriverPacks -Architecture '32-bit'

I can also query the data within PowerShell directly, for example:

$DPs = Get-HPDriverPacks
$DPs | where {$_.Model -match "1040"} | Select Model,"Windows 10 64-bit, 1909"

To find out which models had driver packs for Windows 10 1909 that have been updated since the 1903 version, I did the following:

$DPs | 
    Where {$_."Windows 10 64-bit, 1909" -ne "-" -and $_."Windows 10 64-bit, 1909" -ne $_."Windows 10 64-bit, 1903"} | 
    Select Model,"Windows 10 64-bit, 1909" |
    Out-GridView

So those are the models whose driver packs I can update in the 1909 OSD task sequence.

You can also download the driver packs from this data too, eg:

$URL = ($DPs | where {$_.Model -match "1040 G6"} | Select -ExpandProperty "Windows 10 64-bit, 1909").Split()[0]
Invoke-WebRequest -Uri $URL -OutFile "C:\temp\$($url.Split('/') | Select -Last 1)"

Get Program Execution History from a ConfigMgr Client with PowerShell

Have you ever been in the situation where something unexpected happens on a users computer and people start pointing their fingers at the ConfigMgr admin and asking “has anyone deployed something with SCCM?” Well, I decided to write a PowerShell script to retrieve the execution history for ConfigMgr programs on a local or remote client. This gives clear visibility of when and which deployments such as applications/programs/task sequences have run on the client and hopefully acquit you (or prove you guilty!)

Program execution history can be found in the registry but it doesn’t contain the name of the associated package, so I joined that data with software distribution data from WMI to give a better view.

You can run the script against the local machine, or a remote machine if you have PS remoting enabled. You can also run it against multiple machines at the same time and combine the data if desired. I recommend to pipe the results to grid view.

Get-CMClientExecutionHistory -Computername PC001,PC002 | Out-GridView
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$false,ValueFromPipelineByPropertyName=$true,ValueFromPipeline=$true)]
[string[]]$ComputerName = $env:COMPUTERNAME
)
Begin
{
$Code = {
# Get Execution History from registry, and package details from WMI
$ExecutionHistoryKey = "HKLM:\SOFTWARE\Microsoft\SMS\Mobile Client\Software Distribution\Execution History"
$ContextKeys = Get-ChildItem $ExecutionHistoryKey | Select ExpandProperty PSChildName
foreach ($ContextKey in $ContextKeys)
{
If ($ContextKey -eq "System")
{
$ContextKey = "Machine"
}
Else
{
$ContextKey = $ContextKey.Replace('','_')
}
[array]$SoftwareDistribution += Get-CimInstance Namespace ROOT\ccm\Policy\$ContextKey ClassName CCM_SoftwareDistribution
}
# Create a datatable to hold the results
$DataTable = New-Object System.Data.DataTable
[void]$DataTable.Columns.Add("ComputerName")
[void]$DataTable.Columns.Add("PackageName")
[void]$DataTable.Columns.Add("PackageID")
[void]$DataTable.Columns.Add("ProgramName")
[void]$DataTable.Columns.Add("DeploymentStatus")
[void]$DataTable.Columns.Add("Context")
[void]$DataTable.Columns.Add("State")
[void]$DataTable.Columns.Add("RunStartTime")
[void]$DataTable.Columns.Add("SuccessOrFailureCode")
[void]$DataTable.Columns.Add("SuccessOrFailureReason")
foreach ($ContextKey in $ContextKeys)
{
If ($ContextKey -ne "System")
{
# Get user context if applicable
$SID = New-Object Security.Principal.SecurityIdentifier ArgumentList $ContextKey
$Context = $SID.Translate([System.Security.Principal.NTAccount])
}
Else
{
$Context = "Machine"
}
$SubKeys = Get-ChildItem "$ExecutionHistoryKey\$ContextKey"
Foreach ($SubKey in $SubKeys)
{
$Items = Get-ChildItem $SubKey.PSPath
Foreach ($Item in $Items)
{
$PackageInfo = $SoftwareDistribution | Where {$_.PKG_PackageID -eq $SubKey.PSChildName -and $_.PRG_ProgramName -eq $Item.GetValue("_ProgramID")} | Select First 1
If ($PackageInfo)
{
$PackageName = $PackageInfo.PKG_Name
$DeploymentStatus = "Active"
}
Else
{
$PackageName = "-Unknown-"
$DeploymentStatus = "No longer targeted"
}
[void]$DataTable.Rows.Add($using:Computer,$PackageName,$SubKey.PSChildName,$Item.GetValue("_ProgramID"),$DeploymentStatus,$Context,$Item.GetValue("_State"),$Item.GetValue("_RunStartTime"),$Item.GetValue("SuccessOrFailureCode"),$Item.GetValue("SuccessOrFailureReason"))
}
}
}
$DataTable.DefaultView.Sort = "RunStartTime DESC"
$DataTable = $DataTable.DefaultView.ToTable()
Return $DataTable
}
}
Process
{
foreach ($Computer in $ComputerName)
{
If ($Computer -eq $env:COMPUTERNAME)
{
$Result = Invoke-Command ScriptBlock $Code
}
Else
{
$Result = Invoke-Command ComputerName $Computer HideComputerName ScriptBlock $Code ErrorAction Continue
}
$Result | Select ComputerName,PackageName,PackageID,ProgramName,DeploymentStatus,Context,State,RunStartTime,SuccessOrFailureCode,SuccessOrFailureReason
}
}
End
{
}

Get Previous and Scheduled Evaluation Times for ConfigMgr Compliance Baselines with PowerShell

I was testing a compliance baseline recently and wanted to verify if the schedule defined in the baseline deployment is actually honored on the client. I set the schedule to run every hour, but it was clear that it did not run every hour and that some randomization was being used.

To review the most recent evaluation times and the next scheduled evaluation time, I had to read the scheduler.log in the CCM\Logs directory, because I could only find a single last evaluation time recorded in WMI.

The following PowerShell script reads which baselines are currently deployed to the local machine, displays a window for you to choose one, then basically reads the Scheduler log to find when the most recent evaluations were and when the next one is scheduled.

Select a baseline
Baseline evaluations
##############################################################
## ##
## Reads the most recent and next scheduled evaluation time ##
## for deployed Compliance Baselines from the Scheduler.log ##
## ##
##############################################################
#requires -RunAsAdministrator
# Get Baselines from WMI
# Excludes co-management policies
Try
{
$Instances = Get-CimInstance Namespace ROOT\ccm\dcm ClassName SMS_DesiredConfiguration Filter "PolicyType!=1" OperationTimeoutSec 5 ErrorAction Stop | Select DisplayName,IsMachineTarget,Name
}
Catch
{
Throw "Couldn't get baseline info from WMI: $_"
}
If ($Instances.Count -eq 0)
{
Throw "No deployed baselines found!"
}
# Datatable to hold the baselines for the WPF window
$DataTable = New-Object System.Data.DataTable
[void]$DataTable.Columns.Add("DisplayName")
[void]$DataTable.Columns.Add("IsMachineTarget")
foreach ($Instance in ($Instances | Sort DisplayName))
{
[void]$DataTable.Rows.Add($Instance.DisplayName,$Instance.IsMachineTarget)
}
# WPF Window for baseline selection
Add-Type AssemblyName PresentationFramework,PresentationCore,WindowsBase
$Window = New-Object System.Windows.Window
$Window.WindowStartupLocation = [System.Windows.WindowStartupLocation]::CenterScreen
$Window.SizeToContent = [System.Windows.SizeToContent]::WidthAndHeight
$window.ResizeMode = [System.Windows.ResizeMode]::NoResize
$Window.Title = "DOUBLE-CLICK A BASELINE TO SELECT"
$DataGrid = New-Object System.Windows.Controls.DataGrid
$DataGrid.ItemsSource = $DataTable.DefaultView
$DataGrid.CanUserAddRows = $False
$DataGrid.IsReadOnly = $true
$DataGrid.SelectionMode = [System.Windows.Controls.DataGridSelectionMode]::Single
$DataGrid.Height = "NaN"
$DataGrid.MaxHeight = "250"
$DataGrid.Width = "NaN"
$DataGrid.AlternatingRowBackground = "#e6ffcc"
$DataGrid.Add_MouseDoubleClick({
$script:SelectedRow = $This.SelectedValue
$Window.Close()
})
$Window.AddChild($DataGrid)
[void]$Window.ShowDialog()
If (!$SelectedRow)
{
Throw "No baseline was selected!"
}
# If the baseline is user-targetted
If ($SelectedRow.row.IsMachineTarget -eq $false)
{
# Get Logged-on user SID
$LogonUIRegPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI"
#Could also use this:
#Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\SMS\CurrentUser -Name UserSID -ErrorAction Stop
$Property = "LastLoggedOnUserSID"
$LastLoggedOnUserSID = Get-ItemProperty Path $LogonUIRegPath Name $Property | Select ExpandProperty $Property
$LastLoggedOnUserSIDUnderscore = $LastLoggedOnUserSID.Replace('','_')
$Namespace = "ROOT\ccm\Policy\$LastLoggedOnUserSIDUnderscore\ActualConfig"
}
Else
{
$Namespace = "ROOT\ccm\Policy\Machine\ActualConfig"
}
# Get assignment info
$BaselineName = $SelectedRow.Row.DisplayName
$Pattern = [Regex]::Escape($BaselineName)
$CIAssignment = Get-CimInstance Namespace $Namespace ClassName CCM_DCMCIAssignment | where {$_.AssignmentName -match $Pattern}
$AssignmentIDs = $CIAssignment | Select AssignmentID,AssignmentName
Write-host "Baseline: $BaselineName" ForegroundColor Magenta
foreach ($AssignmentID in $AssignmentIDs)
{
# Read the scheduler log
$Log = "$env:SystemRoot\CCM\Logs\Scheduler.log"
If ($SelectedRow.row.IsMachineTarget -eq $false)
{
$LogEntries = Select-String Path $Log SimpleMatch "$LastLoggedOnUserSID/$($AssignmentID.AssignmentID)"
}
Else
{
$LogEntries = Select-String Path $Log SimpleMatch "Machine/$($AssignmentID.AssignmentID)"
}
If ($LogEntries)
{
# Get the previous evaluations date/time
$Evaluations = New-Object System.Collections.ArrayList
$EvaluationEntries = $LogEntries | where {$_ -match "SMSTrigger"}
Foreach ($Entry in $EvaluationEntries)
{
$Time = $Entry.Line.Split('=')[1]
$Date = $Entry.Line.Split('=')[2]
$a = $Time.Split()[0].trimend().replace('"','')
$b = $Date.Split()[0].trimend().replace('"','').replace('','/')
$Time = (Get-Date $a).ToLongTimeString()
$Date = [DateTime]"$b $Time"
$LocalDate = Get-Date $date Format (Get-Culture).DateTimeFormat.RFC1123Pattern
[void]$Evaluations.Add($LocalDate)
}
# Get the next scheduled evaluation date/time
$LastEvaluation = $EvaluationEntries | Select Last 1
$date = $LastEvaluation.Line.Split()[8]
$time = $LastEvaluation.Line.Split()[9]
$ampm = $LastEvaluation.Line.Split()[10]
$NextEvaluation = [DateTime]"$date $time $ampm"
$NextEvaluationLocal = Get-Date $NextEvaluation Format (Get-Culture).DateTimeFormat.RFC1123Pattern
# Return the results
Write-Host "Assignment: $($AssignmentID.AssignmentName)" ForegroundColor Green
Write-host "Last Evaluations:"
foreach ($Evaluation in $Evaluations)
{
Write-host " $Evaluation" ForegroundColor Yellow
}
Write-host "Next Scheduled Evaluation:"
Write-Host " $NextEvaluationLocal" ForegroundColor Yellow
}
Else
{
Write-Host "No log entries found!" ForegroundColor Red
}
}