Background
Custom inventory rules in Dell Kace are one of the means used to detect the state of managed installations. Custom inventory rules consist of Kace-specific functions that are executed on clients in the network. If those functions evaluate to either true (or if it returns some kind of text i.e. not null), then the software is considered installed. This is different from the default, proprietary method that automatically determines the installation state for software titles.
By default, Dell Kace uses some kind of propriety algorithm that amalgamates info from the file system, wmi, registry, and other sources to determine the state of installed software. All of this happens automatically via kagent.exe, so if you run into a program that does not show up in inventory, then you have to use a Custom Inventory Rule. In this article, I will go over a little-known way to leverage PowerShell from within the ShellCommandTextReturn function. This will enable you to use any data\algorithm available in PowerShell to determine the status of anything.
Guidelines for PowerShell Use in ShellCommandTextReturn
Below, you will find 3 rules that will make writing PowerShell commands from within Custom Inventory Rules easier.
1. Nullify Error Messages
The ShellCommandTextReturn function considers any text as proof of software titles, so control for unnecessary error messages. This can be done by running PS commands from within the scriptblock parameter of invoke-command, then make it silently continue on errors:
ShellCommandTextReturn(powershell.exe "invoke-command –ScriptBlock {Command} ErrorAction SilentlyContinue" 2> nul)
NOTE:
2>null
This helps to nullify any unforeseen error messages from surfacing.
2. Beware of PowerShell Bitness
PowerShell.exe resides under system32 on 32-bit Windows. On 64-bit Windows, there is an x64 PowerShell executable under system32 and a second x86 version of PowerShell in WOW64. Because kagent.exe runs as a 32-bit process, The Windows Redirector will always redirect any system32 directory calls to the WOW64 directory, which contains 32-bit system files. This is done for compatibility reasons. Unfortunately, this can cause problems as some commandlets can only function under the native bitness of the host operating system. For example, the Get-WindowsOptionalFeature commandlet will fail if it is run from the x86 version of PowerShell on 64-bit Windows.
To prevent Windows from automatically redirecting to the 32-bit version of PowerShell, we have to explicitly refer to the 64-bit executable of PowerShell from the Kace ShellCommandTextReturn function. This is done by referencing the sysnative link which is an alias for the system32 directory that contains 64-bit files on 64-bit Windows:
ShellCommandTextReturn(c:\windows\sysnative\WindowsPowerShell\v1.0\powershell.exe etc…)
The command above will only operate as designed for 64-bit systems. 32-bit systems do not recognize the sysnative link and so would error out. If you nullify errors as prescribed by rule #1, the result is that this Custom Inventory Rule will simply ignore 32-bit systems.
To make a Custom Inventory Rule that will work across both 64 and 32-bit systems, call the ShellCommandTextReturn twice with an “or” operator between them. Within one of the ShellCommandTextReturn functions, reference the sysnative link and in the other, use the relative “Powershell.exe” path. PowerShell.exe should always resolve to the appropriate location on 32-bit systems:
ShellCommandTextReturn(c:\windows\sysnative\WindowsPowerShell\v1.0\powershell.exe etc…) or ShellCommandTextReturn(powershell.exe etc…)
3. Beware of Parsing Errors
The general rule of thumb is to alternate between single and double quotation marks when nesting quotes. Nesting is when you have quotes inside of other quotes.
Example:
Good: “Hello ‘Bob’ ” ‘Hello “Bob” ’ “Hello ‘Bob’, ‘Susan’, ‘Anne’ ”
Bad: “Hello “Bob” ” ‘Hello ‘Bob’ ’
The reason for this is that the ShellCommandTextReturn function parses commands from left to right and will complete a set of quotation marks as soon as it encounters a matching set of marks. This will lead to errors, even if that same command works from within a native PowerShell prompt.
So, in the “Hello “Bob”” example, it will try to evaluate “Hello ” and then Bob”” which isn’t a proper string\command. When in reality, we want to evaluate “Bob” first -due to order of operations- and then: “Hello (result of “Bob”)” next.
Final piece of advice would be to alternate quotes strategically, as double quotes are able to evaluate variables and other commands from within strings, while single quotes just outputs strings verbatim.
Use Case: .Net 3.5 Framework Custom Inventory Rule
Knowing these 3 rules, we may build commands leveraging PowerShell to pull all manner of data from WMI, the registry, file system, event logs, Active Directory, and\or any other available source for inventory.
I will use the .Net Framework 3.5 feature on Windows 8.1 as an example for the rest of this post. I chose .Net because it showcases many pitfalls when executing PowerShell commands in this context. Also, .Net is considered a feature and not an application which goes to show that anything in PowerShell may be used as “Software title(s)” in Kace.
We can start by working from a native PowerShell prompt to determine the state of .Net. After some googling, I found that there’s a Get-WindowsOptionalFeature cmdlet that will return this object:
Get-WindowsOptionalFeature -FeatureName 'NetFx3'-Online
As you can see from the image above, the machine has the feature disabled, as per the “State” field. Also notice that the command, as it sits, returns ALL of the text above. Remember, ShellCommandTextReturn interprets any kind of text as proof of a software title (Rule #1). An easy way to control the output would be to:
- Filter the PowerShell text for the “State” property.
- Create an if\then conditional:
- IF the “State” property equals: “Enabled” or “EnabledPending”, return any text\string.
- ELSE Return Null\nothing.
Filter the text out for the “State” property
This is easy, just pipe the command to:
| Select -ExpandProperty ‘State’
Which leaves us with this command:
Get-WindowsOptionalFeature -Online -FeatureName 'NetFx3' | Select -ExpandProperty ‘State’
It should return the current State.
Create an if\then conditional
Now that our command returns the state of .Net, we need to compare the state against “Enabled” and “EnabledPending”. This is done by wrapping the code in a set of parentheses and using the “-in” operator to check against those strings.
(Get-WindowsOptionalFeature -Online -FeatureName 'NetFx3' | select -ExpandProperty ‘State’) -in 'EnablePending','Enabled'
This will output either true or false. We can use the truth value from the command above, as part of a conditional that outputs text if true, or withholds all output if false. To do so, the entire thing has to be placed in another set of parentheses, as part of the if(this){then that}else{$null} construct:
If (PowerShell code) {Return ‘Enabled’} else {Return $null}
NOTE: The ‘Enabled’ string in {Return ‘Enabled’} is arbitrary, it could be anything like {Return ‘Dog’}. It doesn’t matter because, as we’ve established by rule #1, ShellCommandTextReturn accepts any text as proof of software.
Together it looks like this:
If ((Get-WindowsOptionalFeature -Online -FeatureName 'NetFx3' | select -ExpandProperty ‘State’) -in 'EnablePending','Enabled') {Return 'Enabled'} else {Return $null}
Using the command in ShellCommandTextReturn
Now it’s just a matter of invoking this command from the ShellCommandTextReturn function. This can be done by calling the PowerShell executable with Invoke-Command, specifying our code as the script block. Invoke-Command also supports error handling, so using -ErrorAction SilentlyContinue after the script block will help to suppress unwanted text output. Finally, using 2> nul at the end of the command is added assurance that any command line type messages are nullified.
ShellCommandTextReturn(powershell.exe "invoke-command -ScriptBlock {PowerShell Code} - ErrorAction SilentlyContinue" 2> nul)
In keeping with rule #2, we should include another ShellCommandTextReturn function within this Custom Inventory Rule, to support 64-bit Windows:
ShellCommandTextReturn(c:\windows\sysnative\WindowsPowerShell\v1.0\powershell.exe "invoke-command -ScriptBlock {PowerShell Code} - ErrorAction SilentlyContinue" 2> nul) or ShellCommandTextReturn(powershell.exe "invoke-command -ScriptBlock {PowerShell Code} - ErrorAction SilentlyContinue" 2> nul)
Final Product
Now plugging in our PowerShell code from earlier (in curly brackets, below) we get:
ShellCommandTextReturn(c:\windows\sysnative\WindowsPowerShell\v1.0\powershell.exe "invoke-command -ScriptBlock {if ((Get-WindowsOptionalFeature -Online -FeatureName 'NetFx3').State -in 'EnablePending','Enabled') {Return 'Enabled'} else {Return $null}} -ErrorAction SilentlyContinue" 2> nul) or ShellCommandTextReturn(powershell.exe "invoke-command -ScriptBlock {if ((Get-WindowsOptionalFeature -Online -FeatureName 'NetFx3').State -in 'EnablePending','Enabled') {Return 'Enabled'} else {Return $null}} -ErrorAction SilentlyContinue" 2> nul)
This block of jibber-jabber is a completely legal Custom Inventory Rule that detects the enablement of .NET 3.5 on Windows 8\8.1 systems.
Conclusion
If you are able to follow this example, most other commands will be much easier by comparison. As mentioned earlier, we can inventory things like active directory objects, registry settings, files, etc… and deploy scripts based on their Software Title as part of a Managed Installation. The benefit to doing this rather than simply running a script is that Managed Installs are deployed with multiple attempts until it is successful. Also, you can create generic Software Titles that work between minor version changes. It’s only limited by your code.
Additional Notes
Avoid Win32_Product
As tempting as it is, try to avoid referencing the Win32_Product WMI object in your code. Win32_Product queries run very slowly which makes Kace deployments\inventory reports feel slower than normal. Another problem is that every time the Win32_Product is called, it creates a list of 1033 msi event log entries. And because kagent.exe executes inventory checks every few hours, the event log will be filled with useless crap like this:
I haven’t notice any performance hit on end-systems when running a Win32_Product query, so if you routinely clear these entries from the event log, AND don’t care for speed of deployment, then perhaps Win32_Product will work for you.
Use the System Account
Always write your code using the system account on a test computer. You can do this by downloading psexec and running: psexec –ids c:\windows\system32\WindowsPowerShell\v1.0\PowerShell_ISE.exe
From PowerShell, run whoami to verify the user context
Using the system account will simulate the privileges used when kagent.exe executes the code.
Troubleshooting
To troubleshoot your Custom Inventory Rule, enable debug logging on a target system. From there, you may look at the kagent.log and read the text output for the Custom Inventory Rule. It will give you an insight as to what’s going on, more information here:
https://support.software.dell.com/k1000-systems-management-appliance/kb/112035
This website was excellent. I’m testing this command here to look for a file in the appdata directory.
powershell.exe “Invoke-Command {$users=Get-ChildItem C:\Users -Directory ; $users= $users | ?{$_.name -notmatch “Public”} ; Foreach ($u in $users) {$appdata= $u.fullname + “\appdata\roaming”; Get-ChildItem -Path $appdata | ?{$_.name -like ‘filename.exe’} }} -ErrorAction SilentlyContinue” 1>nul
I appreciate the comment!
This helped me a TON! Very detailed article. Thanks for all the hard work. I ended up using the script below to identify any user accounts that were manually added to the local administrators group on a workstation. Made reporting very easy.
ShellCommandTextReturn(c:\windows\sysnative\WindowsPowerShell\v1.0\powershell.exe “invoke-command -ScriptBlock {Get-LocalGroupMember -Name Administrators | Where-object -Property ObjectClass -EQ ‘User’ | Where-Object -Property PrincipalSource -NE Local} -ErrorAction SilentlyContinue” 2> nul) or ShellCommandTextReturn(powershell.exe “invoke-command -ScriptBlock {Get-LocalGroupMember -Name Administrators | Where-object -Property ObjectClass -EQ ‘User’ | Where-Object -Property PrincipalSource -NE Local} -ErrorAction SilentlyContinue” 2> nul)