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($env:COMPUTERNAME,$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
{
}