Categories
PowerShell Tanium

What Makes a Healthy Client?

Update: I received some feedback from the inimitable Tanium TAM Jason Wasser and integrated his recommendation that we report on module injections into the Tanium Client process itself. This allows one to see what dlls have injected themselves into the process and is great for troubleshooting suspected antivirus interference in client operations.

I think that it’s safe to say that we’ve all found ourselves asking what ‘healthy’ actually means to us personally and placing a higher priority on pursuing that ambiguous outcome a little more aggressively than we may have used to. That said, this really isn’t going to be a discussion on that topic.

What I’m more interested in discussing is a problem that I think every administrator of a client-based platform has had throughout their careers. What is client health? What does it mean for a client to be healthy?

For the context of my environment, I began listing out the factors that contribute to create a “healthy” client as I would view it:

  1. What is the client version?
  2. What is the client directory default path? Is it present?
  3. What is the client registry default path? Is it present?
  4. What tags are applied to this client?
  5. Is the client service present? Is it running? How is it configured to start?
  6. What Tanium server is the client configured to communicate with? What’s the ServerNameList?
  7. Is a Test-NetConnection on port 17472 to the Tanium server name successful?
  8. Is the PUB key present?
  9. When was the PUB key created?
  10. When was the last client action performed?
  11. Is the Patch folder present?
  12. How old is the patch manager?
  13. What maintenance windows are applied to this endpoint?
  14. What patching methodology is being used?
  15. If the CAB is the patching methodology, what is the hash of the wsusscn2.cab file?
  16. What are the scan statuses?
  17. If I were to make a summary based on the above information, what would it be?

A modular client such as the Tanium client can “break” in some ways while still largely functioning. Since I generally understand the mechanisms that collectively come together to create a “healthy” client, it seemed relatively straightforward to create a script to spit out those results.

This script is intended not just for Tanium administrators but for adjacent colleagues (Technicians, other Systems Administrators/Engineers, InfoSec, etc.) to help them rule out a misbehaving client if/when they’re investigating an issue. The script is useful even if they still contact the Tanium administrators since providing the results to you up front is effectively doing the groundwork for you.

<#
.SYNOPSIS
This script provides an overview of the state of a local Tanium client and captures key indicators that the DW-Device Management and Delivery Team uses to evaluate client health.
Copyright (C) 2020 - Brent Henderson
Version: 0.0.1.3
#>

#Requires -RunAsAdministrator

[version]$scriptVersion = '0.0.1.3'

try {
if (Test-Path -Path "\\ballast\safeinstall$\tanium\scriptmetadata.csv") {
$csv = Import-Csv -Path "\\ballast\safeinstall$\tanium\scriptmetadata.csv" -ErrorAction SilentlyContinue

}
elseif (Test-Path -Path "\\192.168.3.7\Work\rj\Operations\scriptmetadata.csv") {
$csv = Import-Csv -Path "\\192.168.3.7\Work\rj\Operations\scriptmetadata.csv" -ErrorAction SilentlyContinue
}
}
catch {

}

################## Format-Color Function Loader ##################

function Format-Color([hashtable] $Colors = @{}, [switch] $SimpleMatch) {
$lines = ($input | Out-String) -replace "`r", "" -split "`n"
foreach($line in $lines) {
$color = ''
foreach($pattern in $Colors.Keys){
if(!$SimpleMatch -and $line -match $pattern) { $color = $Colors[$pattern] }
elseif ($SimpleMatch -and $line -like $pattern) { $color = $Colors[$pattern] }
}
if($color) {
Write-Host -ForegroundColor $color $line
} else {
Write-Host $line
}
}
}

################## Configuration State Custom Object ##################

$configurationInfo = [PSCustomObject]@{
'Client Version'=$null;
'Client Directory Default Path'=$null;
'Client Directory Path Validation'=$null;
'Client Registry Default Path'=$null;
'Client Registry Path Validation'=$null;
'Client Registry Tags'=$null;
'Client Service Status'=$null;
'Client Service StartType'=$null;
'Client Module Injections'=$null;
'Tanium Server Name'=$null;
'Tanium Server Name List'=$null;
'Tanium Server NetConnection Test'=$null;
'Pub Key Detected'=$null;
'Pub Key Creation Date'=$null;
'Last Action Date (Minutes)'=$null;
'Patch Folder Detected'=$null;
'Patch Manager Age (Days)'=$null;
'Patch Maintenance Windows'=$null;
'Patch Scan Methodology'=$null;
'WSUS CAB SHA256 Hash'=$null;
'Scan Statuses'=$null;
'Summary'=$null;
}

################## OS Architecture ##################

if ([System.Environment]::Is64BitOperatingSystem) {
$clientRegPath = 'HKLM:\SOFTWARE\WOW6432Node\tanium'
$clientDirectoryPath = 'C:\Program Files (x86)\Tanium\Tanium Client'
$configurationInfo.'Client Directory Default Path' = $clientDirectoryPath
$configurationInfo.'Client Registry Default Path' = $clientRegPath
$poshEXE = 'C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe'
}
else
{
$clientRegPath = 'HKLM:\SOFTWARE\tanium'
$clientDirectoryPath = 'C:\Program Files\Tanium\Tanium Client'
$configurationInfo.'Client Directory Default Path' = $clientDirectoryPath
$configurationInfo.'Client Registry Default Path' = $clientRegPath
$poshEXE = 'C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe'
}

################## Client Version Detection ##################

try {
$configurationInfo.'Client Version' = Get-Item "$clientDirectoryPath\TaniumClient.exe" -ErrorAction Stop | Select-Object -ExpandProperty VersionInfo | Select-Object -ExpandProperty FileVersion
}
catch [System.Management.Automation.ItemNotFoundException]{
$configurationInfo.'Client Version' = 'ItemNotFound'
}

################## Client Directory Validation ##################

$configurationInfo.'Client Directory Path Validation' = Test-Path -Path $clientDirectoryPath

################## Client Registry Validation ##################

$configurationInfo.'Client Registry Path Validation' = Test-Path -Path $clientRegPath

################## Client Tags ##################

try {
$clientTags = Get-Item -Path "$clientRegPath\Tanium Client\Sensor Data\Tags" -ErrorAction Stop | Select-Object -ExpandProperty Property -ErrorAction Stop

$tagString = $null

if ($null -ne $clientTags) {
foreach ($tag in $clientTags) {
$tagString = -join ($tagString,$tag,' <> ')
}

$configurationInfo.'Client Registry Tags' = $tagString.Substring(0,$tagString.Length-4)
}

}
catch [System.Management.Automation.ItemNotFoundException]{
$configurationInfo.'Client Registry Tags' = 'ItemNotFound'
}

################## Client Service Status ##################

try {
$configurationInfo.'Client Service Status' = Get-Service -Name 'Tanium Client' -ErrorAction Stop | Select-Object -ExpandProperty Status
}
catch [Microsoft.PowerShell.Commands.ServiceCommandException]{
$configurationInfo.'Client Service Status' = 'ItemNotFound'
}

################## Client Service Startup Configuration ##################

try {
$configurationInfo.'Client Service StartType' = Get-Service -Name 'Tanium Client' -ErrorAction Stop | Select-Object -ExpandProperty StartType
}
catch [Microsoft.PowerShell.Commands.ServiceCommandException]{
$configurationInfo.'Client Service StartType' = 'ItemNotFound'
}

################# Client Module Injections ##################

try {
$moduleNames = Invoke-Expression -Command (-join ($poshEXE,' -Command {try {Get-Process -Name TaniumClient -Module -ErrorAction Stop | Where-Object {$_.FileName -like "*.dll" ', `
'-and $_.FileName -notlike "*\WInDOWS\SYSTEM32\*" -and $_.FileName -notlike "*Tanium*" -and $_.FileName -notlike "*vcr*"} | Select-Object -ExpandProperty ModuleName -Unique | Sort-Object }
catch [System.Management.Automation.RuntimeException] {Write-Output "AccessDenied"} catch [System.ComponentModel.Win32Exception] {Write-Output "AccessDenied"}} -NoProfile'))

$moduleNameString = $null

if ($null -ne $moduleNames -and $moduleNames -ne 'AccessDenied') {
foreach ($name in $moduleNames) {
switch ($name) {
MPCLIENT.DLL {
$name = $name.Replace('MPCLIENT.DLL','MPCLIENT.DLL (Defender)')
}
MpOav.dll {
$name = $name.Replace('MpOav.dll','MpOav.dll (Defender)')
}
CyKNPHDOJQHQZ.dll {
$name = $name.Replace('CyKNPHDOJQHQZ.dll','CyKNPHDOJQHQZ.dll (Cylance)')
}
CyMemDef.DLL {
$name = $name.Replace('CyMemDef.DLL','CyMemDef.DLL (Cylance)')
}
CyNTFMIHYBLXA.dll {
$name = $name.Replace('CyNTFMIHYBLXA.dll','CyNTFMIHYBLXA.dll (Cylance)')
}
}
$moduleNameString = -join ($moduleNameString,$name,' <> ')
}
if ($null -ne $moduleNameString){
$configurationInfo.'Client Module Injections' = $moduleNameString.Substring(0,$moduleNameString.Length-4)
}
else {
$configurationInfo.'Client Module Injections' = 'NoInjectionsDetected'
}
}
else {
$configurationInfo.'Client Module Injections' = 'AccessDenied'
}
}
catch [System.Management.Automation.RuntimeException] {
$configurationInfo.'Client Module Injections' = 'AccessDenied'
}
catch [System.ComponentModel.Win32Exception] {
$configurationInfo.'Client Module Injections' = 'AccessDenied'
}
catch [Microsoft.PowerShell.Commands.ProcessCommandException] {
$configurationInfo.'Client Module Injections' = 'ProcessNotFound'
}

################## Client Version ##################

try {
$configurationInfo.'Client Version' = Get-Item "$clientDirectoryPath\TaniumClient.exe" -ErrorAction Stop | Select-Object -ExpandProperty VersionInfo | Select-Object -ExpandProperty FileVersion
}
catch [System.Management.Automation.ItemNotFoundException]{
$configurationInfo.'Client Version' = 'ItemNotFound'
}

################## Active Client Server Name ##################

try {
$configurationInfo.'Tanium Server Name' = Get-ItemProperty -Path "$clientRegPath\Tanium Client" -ErrorAction Stop | Select-Object -ExpandProperty ServerName -ErrorAction Stop
}
catch [System.Management.Automation.PSArgumentException],[System.Management.Automation.RuntimeException] {
$configurationInfo.'Tanium Server Name' = 'ItemNotFound'
}

################## Server Name List ##################

try {
$configurationInfo.'Tanium Server Name List' = Get-ItemProperty -Path "$clientRegPath\Tanium Client" -ErrorAction Stop | Select-Object -ExpandProperty ServerNameList -ErrorAction Stop
}
catch [System.Management.Automation.PSArgumentException],[System.Management.Automation.RuntimeException] {
$configurationInfo.'Tanium Server Name List' = 'ItemNotFound'
}

################## Test-NetConnection to Tanium Server ##################

if ([bool]$(Get-Command -Name Test-NetConnection)) {
try {
if ($(Test-NetConnection -ComputerName $configurationInfo.'Tanium Server Name' -Port 17472 -ErrorAction Stop).TcpTestSucceeded -eq 'True'){
$configurationInfo.'Tanium Server NetConnection Test' = $true
}
else {
$configurationInfo.'Tanium Server NetConnection Test' = $false
}
}
catch [System.ComponentModel.Win32Exception] {
$configurationInfo.'Tanium Server NetConnection Test' = 'Error'
}
catch {
Write-Output "$($Error[0].Exception)"
}

}
else {
$configurationInfo.'Tanium Server NetConnection Test' = 'Command Not Available'
}

################## PUB Key Detection ##################

if ([bool]$(Get-Item -Path "$clientDirectoryPath\tanium.pub")) {
$configurationInfo.'Pub Key Detected' = $true
$configurationInfo.'Pub Key Creation Date' = Get-Item -Path "$clientDirectoryPath\tanium.pub" | Select-Object -ExpandProperty CreationTime
}
else {
$configurationInfo.'Pub Key Detected' = $false
$configurationInfo.'Pub Key Creation Date' = 'ItemNotFound'
}

################## Last Client Action Age - C:\Program Files (x86)\Tanium\Tanium Client\Downloads ##################

try {
$lastActionWriteTime = Get-Item -Path "$clientDirectoryPath\Downloads\*" -ErrorAction Stop | Sort-Object -Property lastwritetime | Select-Object -Last 1 | Select-Object -ExpandProperty lastwritetime

if ($lastActionWriteTime -ne $null) {
$configurationInfo.'Last Action Date (Minutes)' = [math]::Round($(New-TimeSpan -Start $lastActionWriteTime -End (Get-Date) | Select-Object -ExpandProperty TotalMinutes))
}
else {
$configurationInfo.'Last Action Date (Minutes)' = 'ItemNotFound'
}
}
catch [System.Management.Automation.ItemNotFoundException]{
$configurationInfo.'Last Action Date (Minutes)' = 'ItemNotFound'
}

################## Patch Folder Validation ##################

$configurationInfo.'Patch Folder Detected' = [bool]$(Test-Path -Path "$clientDirectoryPath\Patch")

################## Patch Tools (Patch Manager vbs) Age ##################

try {
$patchManagerWriteTime = Get-Item -Path "$clientDirectoryPath\Patch\tools\run-patch-manager.min.vbs" -ErrorAction Stop | Select-Object -ExpandProperty lastwritetime
if ($patchManagerWriteTime -ne $null) {
$configurationInfo.'Patch Manager Age (Days)' = [math]::Round($(New-TimeSpan -Start $patchManagerWriteTime -End (Get-Date) | Select-Object -ExpandProperty TotalDays))
}
else {
$configurationInfo.'Patch Manager Age (Days)' = 'ItemNotFound'
}

}
catch [System.Management.Automation.ItemNotFoundException] {
$configurationInfo.'Patch Manager Age (Days)' = 'ItemNotFound'
}

################## Maintenance windows ##################

try {
$maintenanceWindows = Get-Item -Path "$($configurationInfo.'Client Directory Default Path')\Patch\maintenance-windows\configurations\*" -Include *.xml | Select-Object -ExpandProperty FullName

$maintenanceWindowString = $null

if ($null -ne $maintenanceWindows) {
try {
foreach ($window in $maintenanceWindows) {
[xml]$tempWindowVar = Get-Content -Path "$window" -ErrorAction Stop
$maintenanceWindowString = -join ($maintenanceWindowString,$tempWindowVar.MaintenanceWindow.name,'-',$tempWindowVar.MaintenanceWindow.startTime,'-',$tempWindowVar.MaintenanceWindow.endTime,' <> ')
}
}
catch {

}

$configurationInfo.'Patch Maintenance Windows' = $maintenanceWindowString.Substring(0,$maintenanceWindowString.Length-4)
}

}
catch {

}

################## Scan Methodology ##################

try {
$patchScanTools = Get-Item -Path "$clientDirectoryPath\Patch\scans\*" -Include *.cab,*.xml,*.vbs -ErrorAction Stop

if ([bool]$patchScanTools -ne $null) {
if ([bool]$($patchScanTools -match 'tsw-manifest.xml')){
$configurationInfo.'Patch Scan Methodology' = 'Tanium Scan for Windows'
$configurationInfo.'WSUS CAB SHA256 Hash' = 'Not Applicable'
}
elseif ([bool]$($patchScanTools -match 'wsusscn2.cab')) {
$configurationInfo.'Patch Scan Methodology' = 'WSUS Offline CAB'
if ([bool]$(Get-Command -Name Get-FileHash)) {
$configurationInfo.'WSUS CAB SHA256 Hash' = Get-FileHash -Path "$clientDirectoryPath\Patch\scans\wsusscn2.cab" -Algorithm SHA256 -ErrorAction Stop | Select-Object -ExpandProperty Hash
}
else {
$configurationInfo.'WSUS CAB SHA256 Hash' = 'Command Not Available'
}
}
else {
$configurationInfo.'Patch Scan Methodology' = 'ItemNotFound'
}
}
else {
$configurationInfo.'Patch Scan Methodology' = 'ItemNotFound'
}
}
catch [System.Management.Automation.ItemNotFoundException]{
$configurationInfo.'Patch Scan Methodology' = 'ItemNotFound'
}

try {
[xml]$scanStatuses = Get-Content -Path "$clientDirectoryPath\Patch\scans\scan-statuses" -ErrorAction Stop

$scanStatuses.ScanStatuses.ScanConfiguration | ForEach-Object {
if ($statusVar -eq $null) {
$statusVar = -join ($statusVar,$_.id,'-',$_.status,'-',$_.updatedAt,' <> ')
}
else {
$statusVar = -join ($statusVar,$_.id,'-',$_.status,'-',$_.updatedAt,' <> ')
}
}

$configurationInfo.'Scan Statuses' = $statusVar.Substring(0,$statusVar.Length-4)

Remove-Variable -Name statusVar
}
catch [System.Management.Automation.ItemNotFoundException]{
$configurationInfo.'Scan Statuses' = 'ItemNotFound'
}

################## Summary ##################

if ($configurationInfo.'Client Version' -eq 'ItemNotFound' -and $configurationInfo.'Client Directory Path Validation' -eq $false) {
$configurationInfo.Summary = 'Warning: Client not found. Please copy and install Tanium client from \\ballast\safeinstall$ as adminstrator.'
}
elseif ($configurationInfo.'Client Version' -ne 'ItemNotFound' -and $configurationInfo.'Pub Key Detected' -eq $true `
-and $configurationInfo.'Client Service Status' -eq 'Running' -and $configurationInfo.'Patch Folder Detected' -eq $false `
-and [bool]$($configurationInfo.'Client Registry Tags' -notlike "*Patch*")) {
$configurationInfo.Summary = "Warning: Client appears functional but is missing Patch tag. Please contact DW-DMD or add a string value named Patch to $clientRegPath\Tanium Client\Sensor Data\Tags. Patch module and folder should appear within 30 minutes."
}
elseif ($configurationInfo.'Client Directory Path Validation' -eq $true -and $configurationInfo.'Pub Key Detected' -eq $false) {
$configurationInfo.Summary = 'Warning: Client installation folder detected but PUB key is absent. Please copy and reinstall Tanium client from \\ballast\safeinstall$ as administrator.'
}
elseif ($configurationInfo.'Client Version' -ne 'ItemNotFound' -and $configurationInfo.'Pub Key Detected' -eq $true `
-and $configurationInfo.'Client Service Status' -eq 'Running' -and $configurationInfo.'Patch Folder Detected' -eq $true `
-and [bool]$($configurationInfo.'Client Registry Tags' -like "*Patch*") -and $configurationInfo.'Last Action Date (Minutes)' -le '60') {
$configurationInfo.Summary = 'Notice: Client installation appears to be healthy.'
}

################## Out-of-Date Warning ##################

if ($null -ne $csv) {
if ($scriptVersion -lt [version]$csv.currentScriptVersion) {
Write-Host "This version of the script [$($scriptVersion)] is out of date. Please copy the latest version [$($csv.currentScriptVersion)] from \\ballast\safeinstall\tanium."
}
}

################## Color Formatting ##################

$configurationInfo | Format-Color @{
'Automatic' = 'Green';
'Notice*' = 'Green'
'Running' = 'Green';
$true = 'Green';
$false = 'Red';
'AccessDenied' = 'Red';
'ProcessNotFound' = 'Red';
'ItemNotFound' = 'Red';
'MPCLIENT.DLL' = 'Red';
'MpOav.dll' = 'Red';
'PGHook.dll' = 'Red';
'Stopped' = 'Red';
'Warning*' = 'Red';
'Manual' = 'Yellow';
}

The coloration is achieved with an embedded function sourced from Brad Greco (Here). This little script is likely going to mature into a much larger project that I’ve been planning for a while. Of course, that will require me to complete the large project that I’ve been working on since the last time I posted. More to come on that…