Reading CCMEval Results Directly from a ConfigMgr Client with PowerShell

Since Configuration Manager 2012, a scheduled task is created by the client installer that runs “CCMEval.exe” periodically to check that the client is healthy and all the components it depends on are functioning as they should be. A number of checks are performed and if anything is found amiss, the default setting is to attempt remediation to fix the problem.  The client health status is reported to ConfigMgr and stored in the database.

By default, if a client has previously reported healthy and continues to be so, no new health report will be sent by the client to the management point, therefore the “last health evaluation time” reported in the console for active devices may be many months ago.  At first, this can seem disconcerting as it is tempting to think something may be wrong, but in reality the client is in all probability healthy.

If we check the ccmeval.log on the client, we can see that a report has recently been generated but it is “not necessary to send”.  The current report still exists on the client however as we will see later.

capture

It is possible to set the following registry key on the client which will cause the report to always be sent, that way ConfigMgr will always display the date of the most recent evaluation, but this will generate more traffic from the client and more work for ConfigMgr, especially if your client count is high, so probably isn’t best practice.

HKLM\SOFTWARE\Microsoft\CCM\CcmEval\SendAlways = TRUE

We can see the last health evaluation time from the ConfigMgr console, either in the Devices node under Assets and Compliance, or in the Client Check node under Monitoring.

capture

We can also find this information from the database directly with the following SQL query:


Select NetBiosName,ClientStateDescription,HealthCheckDescription,LastHealthEvaluation
from dbo.v_CH_ClientSummary ch
inner join dbo.v_CH_EvalResults eval on ch.ResourceID = eval.ResourceID
where NetBiosName = 'PC001'

 

capture

Yet if we look at the registry of the client itself (using PowerShell), we can see that it reports a much more recent date for the last evaluation time:


Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\CCM\CcmEval -Name LastEvalTime | Select -ExpandProperty LastEvalTime | Get-Date

capture

So in this case the difference between the last evaluation date in the database and the last evaluation date on the client itself is 12 months!  But that’s not a problem though, right? The client has been healthy for 12 months and hasn’t needed to report any further health data. Or has it?  What if the client is having trouble sending the current health evaluation report, therefore ConfigMgr is unaware of the actual current status?

That should be a rare occurrence since an inability to communicate with the management point will cause issues in other areas of client function and will probably result in the client being marked inactive in the console, but to be sure, lets look at the results of the most recent CCMEval task on the client.  When the CCMEval.exe runs, it creates an xml file called “CCMEvalReport.xml” with the results of the health evaluation at %windir%\CCM.  If you try to open this file however, you are greeted with an access denied warning:

capture

So first we need to take a copy of it, and open the copy.  Reading an XML file in notepad though is not too much fun:

capture

Of course, you can use an XML editor or a browser for better viewing, but since PowerShell can read XML nicely, lets create a PowerShell function that will open and parse this file into readable format for us.  We can then use this function to retrieve the CCMEval results from any local or remote computer.

First, since we need to run as administrator to take a copy of the file, let’s invoke a separate PowerShell instance using the “runas” verb so we don’t need to open an additional console window, and copy the file to a temp location:


$ComputerName -eq $env:COMPUTERNAME
$TargetFile = "$env:temp\CcmEvalReport.xml"
try
    {
        Start-Process -FilePath powershell.exe -ArgumentList "-Command ""&{Copy-Item $env:windir\CCM\CcmEvalReport.xml $TargetFile -Force}""" -Wait -ErrorAction Stop  -Verb Runas -WindowStyle Hidden
     }
catch
     {
          $_.Exception.Message
          continue
     }

Then let’s check that the file was copied and exists:


if (!(test-path $TargetFile))
    {
        Write-Error -Message "Could not locate the CcmEvalReport.xml"
        continue
    }

To load the file into PowerShell we need to create a new XML object and call the load method. Then we can remove the file from the temp location.


$xml = New-Object -TypeName System.Xml.XmlDocument
$xml.Load($TargetFile)
Remove-Item $TargetFile -Force

To run the previous code against either the local or a remote machine, we need to encapsulate the code in a script block, then call it using Invoke-Command. In addition, we will add a prompt for credentials in the case access is denied when using PS remoting:


$Script = {
    $TargetFile = ...
}

if ($ComputerName -eq $env:COMPUTERNAME)
{
    $xml = Invoke-Command -ScriptBlock $Script
}
Else
{
    try
    {
        $xml = Invoke-Command -ScriptBlock $Script -ComputerName $ComputerName -ErrorAction Stop
    }
    catch
    {
        if ($Error[0] | Select-String -Pattern 'Access is denied')
        {
            $Credentials = $host.ui.PromptForCredential('Credentials required', "Access was denied to $ComputerName.  Enter credentials to connect.", '', '')
            $xml = Invoke-Command -ScriptBlock $Script -ComputerName $ComputerName -Credential $Credentials
        }
        Else
        {
            $_.Exception.Message
        }
    }
} 

Now if we browse the $xml object, we can find 2 nodes of importance, a summary node:

capture

And a health check node:

capture

So let’s filter the information we need, and add the results to objects that we can output with our function:


$Checks = $xml.ClientHealthReport.HealthChecks.HealthCheck |
Select-Object -Property @{
    l = 'Test'
    e = {
        $_.Description
    }
}, @{
    l = 'Result'
    e = {
        $_.'#text'
    }
} |
Sort-Object -Property Test

$Summary = $xml.ClientHealthReport.Summary |
Select-Object -Property @{
    l = 'ComputerName'
    e = {
        $ComputerName
    }
}, @{
    l = 'EvaluationTime'
    e = {
        [datetime]($_.EvaluationTime)
    }
}, @{
    l = 'Result'
    e = {
        $_.'#text'
    }
}

Finally, we add this code into a function so we can easily run it against the local computer, or pass a remote computername/s to it as a parameter or along the pipeline to read the CCMEval results from other computers.

Get-CCMEvalResult


function Get-CCMEvalResult
{
    <#             .Synopsis             Get the results of the most recent client health evaluation on a local or remote computer             .DESCRIPTION             Parses the ccmevalreport.xml file into a readable format to view the results of the ccmeval task.  Can be run on the local or remote computer.             .EXAMPLE             Get-CCMEvalResult             Returns the ccmeval results from the local machine             .EXAMPLE             Get-CCMEvalResult -ComputerName PC001             Returns the ccmeval results from a remote machine             .EXAMPLE             'PC001','PC002' | Get-CCMEvalResult             Returns the ccmeval results from a remote machine     #>

    #requires -Version 2

    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $false,
                ValueFromPipeline = $true
        )]
        [string[]]$ComputerName = $env:COMPUTERNAME
    )

    Begin {
        $Script = {
            $TargetFile = "$env:temp\CcmEvalReport.xml"
            try
            {
                Start-Process -FilePath powershell.exe -ArgumentList "-Command ""&{Copy-Item $env:windir\CCM\CcmEvalReport.xml $TargetFile -Force}""" -Wait -ErrorAction Stop  -Verb Runas -WindowStyle Hidden
            }
            catch
            {
                $_.Exception.Message
                continue
            }

            if (!(test-path $TargetFile))
                {
                    Write-Error -Message "Could not locate the CcmEvalReport.xml"
                    continue
                }

            $xml = New-Object -TypeName System.Xml.XmlDocument
            $xml.Load($TargetFile)
            Remove-Item $TargetFile -Force
            return $xml
        }
    }

    Process {
        if ($ComputerName -eq $env:COMPUTERNAME)
        {
            $xml = Invoke-Command -ScriptBlock $Script
        }
        Else
        {
            try
                {
                    $xml = Invoke-Command -ScriptBlock $Script -ComputerName $ComputerName -ErrorAction Stop
                }
            catch
                {
                    if ($Error[0] | Select-String -Pattern 'Access is denied')
                        {
                            $Credentials = $host.ui.PromptForCredential('Credentials required', "Access was denied to $Computername.  Enter credentials to connect.", '', '')
                            $xml = Invoke-Command -ScriptBlock $Script -ComputerName $ComputerName -Credential $Credentials
                        }
                    Else { $_.Exception.Message }
                }
        }

        $Checks = $xml.ClientHealthReport.HealthChecks.HealthCheck |
        Select-Object -Property @{
            l = 'Test'
            e = {
                $_.Description
            }
        }, @{
            l = 'Result'
            e = {
                $_.'#text'
            }
        } |
        Sort-Object -Property Test
        [array]$Summary = $xml.ClientHealthReport.Summary |
        Select-Object -Property @{
            l = 'ComputerName'
            e = {
                $ComputerName
            }
        }, @{
            l = 'EvaluationTime'
            e = {
                [datetime]($_.EvaluationTime)
            }
        }, @{
            l = 'Result'
            e = {
                $_.'#text'
            }
        }

        $Summary
        $Checks | Format-Table
    }
}

Now if I run the function against my local machine, I can see each health test performed and the results, as well as the overall status of the evaluation and the time it was performed.

capture

I can also run it against remote computers like this:


Get-CCMEvalResult -ComputerName 'SRV001'
'PC001','PC001' | Get-CCMEvalResult

To troubleshoot any evaluation failures, view the ccmeval.log (%windir%\CCM) and the ccmsetup-ccmeval.log (%windir%\ccmsetup\Logs).

One thought on “Reading CCMEval Results Directly from a ConfigMgr Client with PowerShell

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.