Creating CDXML Modules (Part 2)

Add powerful WMI query capabilities to your CDXML cmdlets, and return information from method calls.

In our first part I looked at CDXML basics and turned the WMI class Win32_OperatingSystem into a set of simple-to-use cmdlets like Get-WmiOperatingSystem, Set-WmiOperatingSystem, or Invoke-WmiOperatingSystemReboot.

Even calling complex WMI methods like Win32ShutdownTracker() became trivial this way:

# forcefully logging off user with a message and timeout (admin privs required)
Invoke-WmiOperatingSystemWin32ShutdownTracker -Flags Logoff,Force -Timeout 30 -Comment 'I log you off in 30 seconds because I can!' -ReasonCode 99

To use all these new cmdlets (like Invoke-WmiOperatingSystemWin32ShutdownTracker in the example above), simply import the CDXML I created last time (using Import-Module). It’s really simple and takes only 10 seconds.

In this second part, I’ll look at some more advanced features. This time, I’ll focus on the WMI class Win32_Service which represents all services on your system.

Yes, there is a built-in cmdlet Get-Service that you can use to query services. However, the WMI approach, and the CDXML cmdlets like Get-WmiService we are about to create are much more powerful.

They return a lot more information, and they are fully remotable, so you can run them locally or on one or multiple remote systems. Get-Service has only very limited built-in remoting capabilities, and while you can use the universal PowerShell Remoting via Invoke-Command, this causes way more overhead than the light-weight remoting built into WMI cmdlets.

The WMI class Win32_OperatingSystem always returns exactly one instance so there was no need for sophisticated querying.

With Win32_Service, that is different. There are hundreds of instances, each representing exactly one service on your system.

Why Manual Queries Suck

Of course you can always use PowerShell’s built-in filtering via Where-Object to query for the service you need:

# finding a specific service
Get-CimInstance -ClassName Win32_Service | Where-Object Name -eq 'Spooler'

However this client-side filtering is evil and only a last resort.

Client-Side Filtering Wastes Resources

With the code above, you have asked WMI to provide information about all services, and then you dumped most of the information and just picked the instance where the name matched your requested service name.

This is wasting resources, especially when you do remote queries:

# finding a specific remote service
Get-CimInstance -ClassName Win32_Service -ComputerName webserver12 | Where-Object Name -eq 'Spooler'

This example assumes that your current account has remoting permissions on the target machine, and that remoting has been setup properly. If remote access doesn’t work for you, go over the prerequisites.

WMI on the target machine is now transferring information about all services across the network to you, and you are dumping most of it.

Server-Side Queries Are Better, But…

A much better approach uses server-side queries and asks WMI to produce only the data you really need:

# finding a specific service via server side query:
Get-CimInstance -ClassName Win32_Service -Filter 'Name="Spooler"'
Get-CimInstance -Query 'Select * From Win32_Service Where Name="Spooler"'

This approach does not need the client-side filter Where-Object anymore and is much more efficient. However there is a “but”. Server-side filters do not run within PowerShell. Instead, they run inside whoever produces the data. In our case, the queries run inside WMI.

This is why the filter expression is not using PowerShell and its operators. Instead, a query language named WQL is used (which is very similar to the common database query language SQL). It may become complex to compose WMI filters, and you’d need to use the WQL Operators.

CDXML: Autogenerating Cmdlets

Fortunately, CDXML can produce powerful PowerShell cmdlets that make querying really simple and fast.

Creating Get-WmiService

Here is the XML definition for Get-WmiService in the most simple way, similar to what I did in part 1:

<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
  <Class ClassName="Root/CIMV2/Win32_Service" ClassVersion="2.0">
    <Version>1.0</Version>
    <DefaultNoun>WmiService</DefaultNoun>
    <InstanceCmdlets>
      <GetCmdletParameters />
    </InstanceCmdlets>
  </Class>
</PowerShellMetadata>

The key information is the ClassName (pointing to the WMI namespace and classname I am about to use) and the DefaultNoun for the cmdlets that CDXML will generate for us.

Let’s write the XML to file and import it as a PowerShell module:

$testfolder = "$env:temp\cdxmlTest"

$cdxml = @'
<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
  <Class ClassName="Root/CIMV2/Win32_Service" ClassVersion="2.0">
    <Version>1.0</Version>
    <DefaultNoun>WmiService</DefaultNoun>
    <InstanceCmdlets>
      <GetCmdletParameters />
    </InstanceCmdlets>
  </Class>
</PowerShellMetadata>
'@

# create a test folder if it does not exist
$exists = Test-Path -Path $testfolder -PathType Container
if (!$exists) { $null = New-Item -Path $testfolder -ItemType Directory }

# write cdxml file
$path = Join-Path -Path $testfolder -ChildPath 'win32_service.cdxml'
$cdxml | Set-Content -Path $path -Encoding UTF8

# import the module
Import-Module -Name $path -Verbose -Force

When you run this, auto-magically a new module named Win32_Service is produced and contains one cmdlet named Get-WmiService:

VERBOSE: Loading module from path 'C:\Users\tobia\AppData\Local\Temp\cdxmlTest\win32_service.cdxml'.
VERBOSE: Importing function 'Get-WmiService'.

And when you run Get-WmiService, it returns all instances of Win32_Service:

Get-WmiService | Select-Object -Property * 

That’s a lot more information than Get-Service returns. That’s good:

Name                    : AdobeARMservice
Status                  : OK
ExitCode                : 0
DesktopInteract         : False
ErrorControl            : Ignore
PathName                : "C:\Program Files (x86)\Common Files\Adobe\ARM\1.0\armsvc.exe"
ServiceType             : Own Process
StartMode               : Auto
Caption                 : Adobe Acrobat Update Service
Description             : Adobe Acrobat Updater keeps your Adobe software up to date.
InstallDate             :
CreationClassName       : Win32_Service
Started                 : True
SystemCreationClassName : Win32_ComputerSystem
SystemName              : DELL7390
AcceptPause             : False
AcceptStop              : True
DisplayName             : Adobe Acrobat Update Service
ServiceSpecificExitCode : 0
StartName               : LocalSystem
State                   : Running
TagId                   : 0
CheckPoint              : 0
DelayedAutoStart        : False
ProcessId               : 4244
WaitHint                : 0
PSComputerName          :
CimClass                : Root/CIMV2:Win32_Service
CimInstanceProperties   : {Caption, Description, InstallDate, Name...}
CimSystemProperties     : Microsoft.Management.Infrastructure.CimSystemProperties

...

But where is the advanced querying? Get-WmiService always returns all service instances.

Adding Advanced Querying

Let’s add sophisticated query parameters. In the previous XML definition, the Get-cmdlet was automatically created for us. Let’s define it manually. I am adding a node GetCmdlet below.

With Verb, you would now be able to change the default verb to anything you like but obviously Get was a good choice, so I am keeping this. The interesting part starts in the node QueryableProperties:

<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
  <Class ClassName="Root/CIMV2/Win32_Service" ClassVersion="2.0">
    <Version>1.0</Version>
    <DefaultNoun>WmiService</DefaultNoun>
    <InstanceCmdlets>
      <GetCmdletParameters />
      <GetCmdlet>
        <CmdletMetadata Verb="Get" />
        <GetCmdletParameters DefaultCmdletParameterSet="ByName">
          <QueryableProperties>
            <Property PropertyName="AcceptPause">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="AcceptStop">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="CheckPoint">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="CreationClassName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="DelayedAutoStart">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Description">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="DesktopInteract">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="DisplayName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ErrorControl">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Ignore</AllowedValue>
                    <AllowedValue>Normal</AllowedValue>
                    <AllowedValue>Severe</AllowedValue>
                    <AllowedValue>Critical</AllowedValue>
                    <AllowedValue>Unknown</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="ExitCode">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="InstallDate">
              <Type PSType="system.datetime" />
              <MinValueQuery>
                <CmdletParameterMetadata PSName="BeforeInstallDate" />
              </MinValueQuery>
              <MaxValueQuery>
                <CmdletParameterMetadata PSName="AfterInstallDate" />
              </MaxValueQuery>
            </Property>
            <Property PropertyName="Name">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata Position="0" CmdletParameterSets="ByName" IsMandatory="false" />
              </RegularQuery>
              <ExcludeQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" PSName="ExcludeName" CmdletParameterSets="ByName" />
              </ExcludeQuery>
            </Property>
            <Property PropertyName="PathName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ProcessId">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata Position="0" CmdletParameterSets="ByProcessId" IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ServiceSpecificExitCode">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ServiceType">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Kernel Driver</AllowedValue>
                    <AllowedValue>File System Driver</AllowedValue>
                    <AllowedValue>Adapter</AllowedValue>
                    <AllowedValue>Recognizer Driver</AllowedValue>
                    <AllowedValue>Own Process</AllowedValue>
                    <AllowedValue>Share Process</AllowedValue>
                    <AllowedValue>Interactive Process</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="Started">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="StartMode">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Boot</AllowedValue>
                    <AllowedValue>System</AllowedValue>
                    <AllowedValue>Auto</AllowedValue>
                    <AllowedValue>Manual</AllowedValue>
                    <AllowedValue>Disabled</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="StartName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="State">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Stopped</AllowedValue>
                    <AllowedValue>Start Pending</AllowedValue>
                    <AllowedValue>Stop Pending</AllowedValue>
                    <AllowedValue>Running</AllowedValue>
                    <AllowedValue>Continue Pending</AllowedValue>
                    <AllowedValue>Pause Pending</AllowedValue>
                    <AllowedValue>Paused</AllowedValue>
                    <AllowedValue>Unknown</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="Status">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="TagId">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="WaitHint">
              <Type PSType="system.uint32" />
              <MinValueQuery>
                <CmdletParameterMetadata PSName="MinWaitHint" />
              </MinValueQuery>
              <MaxValueQuery>
                <CmdletParameterMetadata PSName="MaxWaitHint" />
              </MaxValueQuery>
            </Property>
          </QueryableProperties>
        </GetCmdletParameters>
      </GetCmdlet>
    </InstanceCmdlets>
  </Class>
</PowerShellMetadata>

QueryableProperties contains a long list of parameter definitions. Each is a query that you can later use to search for instances. Inside of each parameter, there is a definition for the type of query you want.

Run the script below to update your module Win32_Service and produce a better Get-WmiService cmdlet:

$testfolder = "$env:temp\cdxmlTest"

$cdxml = @'
<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
  <Class ClassName="Root/CIMV2/Win32_Service" ClassVersion="2.0">
    <Version>1.0</Version>
    <DefaultNoun>WmiService</DefaultNoun>
    <InstanceCmdlets>
      <GetCmdletParameters />
      <GetCmdlet>
        <CmdletMetadata Verb="Get" />
        <GetCmdletParameters DefaultCmdletParameterSet="ByName">
          <QueryableProperties>
            <Property PropertyName="AcceptPause">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="AcceptStop">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="CheckPoint">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="CreationClassName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="DelayedAutoStart">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Description">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="DesktopInteract">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="DisplayName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ErrorControl">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Ignore</AllowedValue>
                    <AllowedValue>Normal</AllowedValue>
                    <AllowedValue>Severe</AllowedValue>
                    <AllowedValue>Critical</AllowedValue>
                    <AllowedValue>Unknown</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="ExitCode">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="InstallDate">
              <Type PSType="system.datetime" />
              <MinValueQuery>
                <CmdletParameterMetadata PSName="BeforeInstallDate" />
              </MinValueQuery>
              <MaxValueQuery>
                <CmdletParameterMetadata PSName="AfterInstallDate" />
              </MaxValueQuery>
            </Property>
            <Property PropertyName="Name">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata Position="0" CmdletParameterSets="ByName" IsMandatory="false" />
              </RegularQuery>
              <ExcludeQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" PSName="ExcludeName" CmdletParameterSets="ByName" />
              </ExcludeQuery>
            </Property>
            <Property PropertyName="PathName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ProcessId">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata Position="0" CmdletParameterSets="ByProcessId" IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ServiceSpecificExitCode">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ServiceType">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Kernel Driver</AllowedValue>
                    <AllowedValue>File System Driver</AllowedValue>
                    <AllowedValue>Adapter</AllowedValue>
                    <AllowedValue>Recognizer Driver</AllowedValue>
                    <AllowedValue>Own Process</AllowedValue>
                    <AllowedValue>Share Process</AllowedValue>
                    <AllowedValue>Interactive Process</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="Started">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="StartMode">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Boot</AllowedValue>
                    <AllowedValue>System</AllowedValue>
                    <AllowedValue>Auto</AllowedValue>
                    <AllowedValue>Manual</AllowedValue>
                    <AllowedValue>Disabled</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="StartName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="State">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Stopped</AllowedValue>
                    <AllowedValue>Start Pending</AllowedValue>
                    <AllowedValue>Stop Pending</AllowedValue>
                    <AllowedValue>Running</AllowedValue>
                    <AllowedValue>Continue Pending</AllowedValue>
                    <AllowedValue>Pause Pending</AllowedValue>
                    <AllowedValue>Paused</AllowedValue>
                    <AllowedValue>Unknown</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="Status">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="TagId">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="WaitHint">
              <Type PSType="system.uint32" />
              <MinValueQuery>
                <CmdletParameterMetadata PSName="MinWaitHint" />
              </MinValueQuery>
              <MaxValueQuery>
                <CmdletParameterMetadata PSName="MaxWaitHint" />
              </MaxValueQuery>
            </Property>
          </QueryableProperties>
        </GetCmdletParameters>
      </GetCmdlet>
    </InstanceCmdlets>
  </Class>
</PowerShellMetadata>
'@

# create a test folder if it does not exist
$exists = Test-Path -Path $testfolder -PathType Container
if (!$exists) { $null = New-Item -Path $testfolder -ItemType Directory }

# write cdxml file
$path = Join-Path -Path $testfolder -ChildPath 'win32_service.cdxml'
$cdxml | Set-Content -Path $path -Encoding UTF8

# import the module
Import-Module -Name $path -Verbose -Force

Now querying is super easy, and you can combine all of the query parameters to produce very complex queries - and these queries automatically run on the server-side:

# query for all services that start with "S", are running, and can interact with the user
Get-WmiService -DesktopInteract -Name S* -State Running

If you wanted to compose this query manually that would be a monster task with a lot of expert knowledge (i.e. you’d need to know the property names, the WQL operators, and make sure you use the special wildcard character %):

Get-CimInstance -ClassName Win32_Service -Filter 'DesktopInteract=TRUE AND Name LIKE "S%" AND State="Running"'

Let’s take a quick look at the different query types you can use for your parameters:

Support for Switch Parameters

A RegularQuery matches the value you submit to the parameter. If the value you want to query is a simple boolean value, you can ask PowerShell to add a switch parameter. Here is an example:

<Property PropertyName="DesktopInteract">
    <Type PSType="switch" />
    <RegularQuery AllowGlobbing="false">
        <CmdletParameterMetadata IsMandatory="false" />
    </RegularQuery>
</Property>

The parameter DesktopInteract is a switch parameter, and when you set it, all services are returned where the property DesktopInteract is true. So it will be trivial to find services that can interact with the user:

Get-WmiService -DesktopInteract 

Support For Wildcards

Now take a look at the parameter Name:

<Property PropertyName="Name">
    <Type PSType="system.string" />
    <RegularQuery AllowGlobbing="true">
        <CmdletParameterMetadata Position="0" CmdletParameterSets="ByName" IsMandatory="false" />
    </RegularQuery>
    <ExcludeQuery AllowGlobbing="true">
        <CmdletParameterMetadata IsMandatory="false" PSName="ExcludeName" CmdletParameterSets="ByName" />
    </ExcludeQuery>
</Property>

It matches the instance property Name, so you can now easily search for service names. And because AllowGlobbing is set to $true, wildcards are permitted:

# finding all services that start with "S"
Get-WmiService -Name S*

Support For Excludes

The parameter Name uses a second query ExcludeQuery which does the opposite. So to find all services that do not start with S, run this:

# finding all services that do not start with "S"
Get-WmiService -Name * -ExcludeName S*

Support for Enumerations

Some properties typically contain one of a predefined list of keywords. For example StartMode can only be one of five predefined start modes. Take a look at the property StartMode:

<Property PropertyName="StartMode">
    <Type PSType="system.string" />
    <RegularQuery AllowGlobbing="true">
        <CmdletParameterMetadata IsMandatory="false">
            <ValidateSet>
                <AllowedValue>Boot</AllowedValue>
                <AllowedValue>System</AllowedValue>
                <AllowedValue>Auto</AllowedValue>
                <AllowedValue>Manual</AllowedValue>
                <AllowedValue>Disabled</AllowedValue>
            </ValidateSet>
        </CmdletParameterMetadata>
    </RegularQuery>
</Property>

The parameter supports a ValidateSet, much similar to your own hand-written PowerShell functions, so not only can you query for services with a given start mode. PowerShell also supports auto-completion and intellisense:

# finding all autostart services
Get-WmiService -StartMode Auto

Support For Ranges

When properties contain numbers or dates, you often don’t want to check for a specific number or date but rather use queries that search for numbers less or greater than a value, or before or after a date.

Have a look at the property InstallDate:

<Property PropertyName="InstallDate">
    <Type PSType="system.datetime" />
    <MinValueQuery>
        <CmdletParameterMetadata PSName="BeforeInstallDate" />
    </MinValueQuery>
    <MaxValueQuery>
        <CmdletParameterMetadata PSName="AfterInstallDate" />
    </MaxValueQuery>
</Property>

Here, Get-WmiObject does not implement the search parameter -InstallDate but rather -BeforeInstallDate and -AfterInstallDate using a MinValueQuery and MaxValueQuery. So you now can easily find services installed before or after a given date:

Get-WmiService -AfterInstallDate '2020-01-01'

Unfortunately, most services do not maintain their InstallDate so this property is typically empty.

Calling Methods

Win32_Service comes with 11 methods that you can use for example to start, stop, pause, or resume services. All of this can also be achieved using the built-in PowerShell cmdlets like Start-Service or Set-Service.

However keep in mind: the WMI methods always have the advantage of its built-in remoting support, and some of the methods cover areas that aren’t available with the built-in cmdlets.

I am not going over the details of implementing methods again (please return to part 1 for a refresher). I do want to cover one new feature: returning information from a method.

Returning Information

Most WMI methods invoke some action but they typically always return a default status object with an error code that tells you if the call was successful.

Only a few methods actually return rich information. Win32_Service provides the method GetSecurityDescriptor() which returns the security information attached to a service. This is the Xml definition for a new cmdlet Get-WmiServiceSecurityDescriptor that calls this method:

<Cmdlet>
        <CmdletMetadata Verb="Get" Noun="WmiServiceSecurityDescriptor" ConfirmImpact="Low" />
        <!--defining the WMI instance method used by this cmdlet:-->
        <Method MethodName="GetSecurityDescriptor">
          <ReturnValue>
            <Type PSType="system.uint32" />
            <CmdletOutputMetadata>
              <ErrorCode />
            </CmdletOutputMetadata>
          </ReturnValue>

          <Parameters>
            <!--Method returns an instance of Win32_SecurityDescriptor in property Descriptor-->
            <Parameter ParameterName="Descriptor">
              <Type PSType="Microsoft.Management.Infrastructure.CimInstance[]" />
              <CmdletOutputMetadata />
            </Parameter>
          </Parameters>
        </Method>
      </Cmdlet>

The important part takes place in the node Parameters: Typically, this node defines the input parameters that go into the method you want to call. The method GetSecurityDescriptor does not require any parameters so why is this section still present?

It turns out that the node Parameters also defines the output parameter. If a method call returns additional information, this information surfaces in the generic return object that all methods return.

When you call the method GetSecurityDescriptor manually using the CIM Cmdlets, you can see this:

$query = 'Select * From Win32_Service where Name="Spooler"'

Invoke-CimMethod -Query $query -MethodName GetSecurityDescriptor |
  Add-Member -MemberType ScriptProperty -Name ReturnValueFriendly -Passthru -Value {
  switch ([int]$this.ReturnValue)
  {
        0        {'Success'}
        2        {'Access denied'}
        8        {'Unknown failure'}
        9        {'Privilege missing'}
        21       {'Invalid parameter'}
        default  {"Unknown Error $_"}
    }
}

Make sure you run this code with Administrator privileges! Regular users cannot read security-related information.

The result looks like this:

Descriptor               ReturnValue PSComputerName ReturnValueFriendly
----------               ----------- -------------- -------------------
Win32_SecurityDescriptor           0                Success

The real return information can be found in the property Descriptor. And that is exactly what can be found in the property definition:

<Parameter ParameterName="Descriptor">
    <Type PSType="Microsoft.Management.Infrastructure.CimInstance[]" />
    <CmdletOutputMetadata />
</Parameter>

It tells PowerShell that the property Descriptor yields instances of type Microsoft.Management.Infrastructure.CimInstance.

To test-drive Get-WmiServiceSecurityDescriptor, run this:

$testfolder = "$env:temp\cdxmlTest"

$cdxml = @'
<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
  <Class ClassName="Root/CIMV2/Win32_Service" ClassVersion="2.0">
    <Version>1.0</Version>
    <DefaultNoun>WmiService</DefaultNoun>
    <InstanceCmdlets>
      <GetCmdletParameters />
      <GetCmdlet>
        <CmdletMetadata Verb="Get" />
        <GetCmdletParameters DefaultCmdletParameterSet="ByName">
          <QueryableProperties>
            <Property PropertyName="AcceptPause">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="AcceptStop">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="CheckPoint">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="CreationClassName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="DelayedAutoStart">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Description">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="DesktopInteract">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="DisplayName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ErrorControl">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Ignore</AllowedValue>
                    <AllowedValue>Normal</AllowedValue>
                    <AllowedValue>Severe</AllowedValue>
                    <AllowedValue>Critical</AllowedValue>
                    <AllowedValue>Unknown</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="ExitCode">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="InstallDate">
              <Type PSType="system.datetime" />
              <MinValueQuery>
                <CmdletParameterMetadata PSName="BeforeInstallDate" />
              </MinValueQuery>
              <MaxValueQuery>
                <CmdletParameterMetadata PSName="AfterInstallDate" />
              </MaxValueQuery>
            </Property>
            <Property PropertyName="Name">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata Position="0" CmdletParameterSets="ByName" IsMandatory="false" />
              </RegularQuery>
              <ExcludeQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" PSName="ExcludeName" CmdletParameterSets="ByName" />
              </ExcludeQuery>
            </Property>
            <Property PropertyName="PathName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ProcessId">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata Position="0" CmdletParameterSets="ByProcessId" IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ServiceSpecificExitCode">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ServiceType">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Kernel Driver</AllowedValue>
                    <AllowedValue>File System Driver</AllowedValue>
                    <AllowedValue>Adapter</AllowedValue>
                    <AllowedValue>Recognizer Driver</AllowedValue>
                    <AllowedValue>Own Process</AllowedValue>
                    <AllowedValue>Share Process</AllowedValue>
                    <AllowedValue>Interactive Process</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="Started">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="StartMode">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Boot</AllowedValue>
                    <AllowedValue>System</AllowedValue>
                    <AllowedValue>Auto</AllowedValue>
                    <AllowedValue>Manual</AllowedValue>
                    <AllowedValue>Disabled</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="StartName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="State">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Stopped</AllowedValue>
                    <AllowedValue>Start Pending</AllowedValue>
                    <AllowedValue>Stop Pending</AllowedValue>
                    <AllowedValue>Running</AllowedValue>
                    <AllowedValue>Continue Pending</AllowedValue>
                    <AllowedValue>Pause Pending</AllowedValue>
                    <AllowedValue>Paused</AllowedValue>
                    <AllowedValue>Unknown</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="Status">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="TagId">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="WaitHint">
              <Type PSType="system.uint32" />
              <MinValueQuery>
                <CmdletParameterMetadata PSName="MinWaitHint" />
              </MinValueQuery>
              <MaxValueQuery>
                <CmdletParameterMetadata PSName="MaxWaitHint" />
              </MaxValueQuery>
            </Property>
          </QueryableProperties>
        </GetCmdletParameters>
      </GetCmdlet>

      <!--Get-ServiceSecurityDescriptor: invoking method GetSecurityDescriptor():-->
      <Cmdlet>
        <!--defining the ConfirmImpact which indicates how severe the changes are that this cmdlet performs-->
        <CmdletMetadata Verb="Get" Noun="WmiServiceSecurityDescriptor" ConfirmImpact="Low" />
        <!--defining the WMI instance method used by this cmdlet:-->
        <Method MethodName="GetSecurityDescriptor">
          <ReturnValue>
            <Type PSType="system.uint32" />
            <CmdletOutputMetadata>
              <ErrorCode />
            </CmdletOutputMetadata>
          </ReturnValue>
          <!--defining the parameters of this cmdlet:-->
          <Parameters>
            <!--Method returns an instance of Win32_SecurityDescriptor in property Descriptor-->
            <Parameter ParameterName="Descriptor">
              <Type PSType="Microsoft.Management.Infrastructure.CimInstance[]" />
              <CmdletOutputMetadata />
            </Parameter>
          </Parameters>
        </Method>
      </Cmdlet>

    </InstanceCmdlets>
  </Class>
</PowerShellMetadata>
'@

# create a test folder if it does not exist
$exists = Test-Path -Path $testfolder -PathType Container
if (!$exists) { $null = New-Item -Path $testfolder -ItemType Directory }

# write cdxml file
$path = Join-Path -Path $testfolder -ChildPath 'win32_service.cdxml'
$cdxml | Set-Content -Path $path -Encoding UTF8

# import the module
Import-Module -Name $path -Verbose -Force

As it turns out, Get-WmiServiceSecurityDescriptor has no own query parameters so you’d have to combine it with Get-WmiService like this:

Get-WmiService -Name spooler | Get-WmiServiceSecurityDescriptor

Make sure you run this with full Administrator privileges.

Thanks to the output parameter definition, Get-WmiServiceSecurityDescriptor directly returns the security information:

ControlFlags   : 32788
DACL           : {Win32_ACE, Win32_ACE, Win32_ACE}
Group          : Win32_Trustee
Owner          : Win32_Trustee
SACL           : {Win32_ACE}
TIME_CREATED   :
PSComputerName :

Final Version: Module Win32_Service

To finalize the module, wouldn’t it be nice if Get-WmiServiceSecurityDescriptor could run stand-alone, too? Up until now, all method cmdlets can only work when you pipe instances into them:

# current implementation requires pipeline:
Get-WmiService -Name spooler | Get-WmiServiceSecurityDescriptor

# with query parameters, method cmdlets could work standlone, too:
Get-WmiServiceSecurityDescriptor -Name spooler

Adding Query Parameters For Methods

For this, you’d just have to add more QueryParameters. We already defined a lot of them for GetCmdlet.

You have two choices:

  • All Instance Methods: add a node GetCmdletParameters to InstanceCmdlets. These query parameters are then available to all instance methods.
  • Specific Method: or add a node GetCmdletParameters to the actual Method node of the method where you want them to be added. Query parameters would apply only to that specific method.

Limited Set Of Query Parameters

For the GetCmdlet Get-WmiService, it was a good idea to add as many query parameters as possible, and to be as flexible as possible and include wildcard support. That’s because Get-WmiService is typically used to search for services, and being able to use a variety of search parameters is helpful.

For cmdlets that invoke methods, you should be a lot more restrictive. It makes no sense and can actually be harmful if your query parameters selected a large number of services that then would all invoke the method.

Instead, you should limit yourself to one distinct query parameter, and not allow wildcards - forcing the user to specify the exact service name.

With this restrictive approach, you still would have the full querying flexibility when you use Get-WmiService with its rich querying and pipe the results to other cmdlets.

So I am adding a query parameter Name to all instance cmdlets, and I am setting AllowGlobbing to $false (wildcards not permitted).

As you see, you can use the same query parameter (in this case Name) multiple times: for the GetCmdlet, it would allow wildcards, and for method invocation cmdlets, it would not.

You can use -Name with wildcards, but when you use Get-WmiServiceSecurityDescriptor, you must specify the exact name:

<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
  <Class ClassName="Root/CIMV2/Win32_Service" ClassVersion="2.0">
    <Version>1.0</Version>
    <DefaultNoun>WmiService</DefaultNoun>
    <InstanceCmdlets>
      
      <GetCmdletParameters DefaultCmdletParameterSet="ByName">
        <QueryableProperties>
          <Property PropertyName="Name">
            <Type PSType="system.string" />
            <RegularQuery AllowGlobbing="false">
              <CmdletParameterMetadata Position="0" CmdletParameterSets="ByName" IsMandatory="false" />
            </RegularQuery>
          </Property>
        </QueryableProperties>
      </GetCmdletParameters>

      <GetCmdlet>
        <CmdletMetadata Verb="Get" />
...

Here is another update to your CDXML module which now also contains a method Get-WmiServiceSecurityDescriptor:

$testfolder = "$env:temp\cdxmlTest"

$cdxml = @'
<?xml version="1.0" encoding="utf-8"?>
<PowerShellMetadata xmlns="http://schemas.microsoft.com/cmdlets-over-objects/2009/11">
  <Class ClassName="Root/CIMV2/Win32_Service" ClassVersion="2.0">
    <Version>1.0</Version>
    <DefaultNoun>WmiService</DefaultNoun>
    <InstanceCmdlets>
      
      <GetCmdletParameters DefaultCmdletParameterSet="ByName">
        <QueryableProperties>
          <Property PropertyName="Name">
            <Type PSType="system.string" />
            <RegularQuery AllowGlobbing="false">
              <CmdletParameterMetadata Position="0" CmdletParameterSets="ByName" IsMandatory="false" />
            </RegularQuery>
          </Property>
        </QueryableProperties>
      </GetCmdletParameters>

      <GetCmdlet>
        <CmdletMetadata Verb="Get" />
        <GetCmdletParameters DefaultCmdletParameterSet="ByName">
          <QueryableProperties>
            <Property PropertyName="AcceptPause">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="AcceptStop">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="CheckPoint">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="CreationClassName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="DelayedAutoStart">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="Description">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="DesktopInteract">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="DisplayName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ErrorControl">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Ignore</AllowedValue>
                    <AllowedValue>Normal</AllowedValue>
                    <AllowedValue>Severe</AllowedValue>
                    <AllowedValue>Critical</AllowedValue>
                    <AllowedValue>Unknown</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="ExitCode">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="InstallDate">
              <Type PSType="system.datetime" />
              <MinValueQuery>
                <CmdletParameterMetadata PSName="BeforeInstallDate" />
              </MinValueQuery>
              <MaxValueQuery>
                <CmdletParameterMetadata PSName="AfterInstallDate" />
              </MaxValueQuery>
            </Property>
            <Property PropertyName="Name">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata Position="0" CmdletParameterSets="ByName" IsMandatory="false" />
              </RegularQuery>
              <ExcludeQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" PSName="ExcludeName" CmdletParameterSets="ByName" />
              </ExcludeQuery>
            </Property>
            <Property PropertyName="PathName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ProcessId">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata Position="0" CmdletParameterSets="ByProcessId" IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ServiceSpecificExitCode">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="ServiceType">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Kernel Driver</AllowedValue>
                    <AllowedValue>File System Driver</AllowedValue>
                    <AllowedValue>Adapter</AllowedValue>
                    <AllowedValue>Recognizer Driver</AllowedValue>
                    <AllowedValue>Own Process</AllowedValue>
                    <AllowedValue>Share Process</AllowedValue>
                    <AllowedValue>Interactive Process</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="Started">
              <Type PSType="switch" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="StartMode">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Boot</AllowedValue>
                    <AllowedValue>System</AllowedValue>
                    <AllowedValue>Auto</AllowedValue>
                    <AllowedValue>Manual</AllowedValue>
                    <AllowedValue>Disabled</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="StartName">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="State">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false">
                  <ValidateSet>
                    <AllowedValue>Stopped</AllowedValue>
                    <AllowedValue>Start Pending</AllowedValue>
                    <AllowedValue>Stop Pending</AllowedValue>
                    <AllowedValue>Running</AllowedValue>
                    <AllowedValue>Continue Pending</AllowedValue>
                    <AllowedValue>Pause Pending</AllowedValue>
                    <AllowedValue>Paused</AllowedValue>
                    <AllowedValue>Unknown</AllowedValue>
                  </ValidateSet>
                </CmdletParameterMetadata>
              </RegularQuery>
            </Property>
            <Property PropertyName="Status">
              <Type PSType="system.string" />
              <RegularQuery AllowGlobbing="true">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="TagId">
              <Type PSType="system.uint32" />
              <RegularQuery AllowGlobbing="false">
                <CmdletParameterMetadata IsMandatory="false" />
              </RegularQuery>
            </Property>
            <Property PropertyName="WaitHint">
              <Type PSType="system.uint32" />
              <MinValueQuery>
                <CmdletParameterMetadata PSName="MinWaitHint" />
              </MinValueQuery>
              <MaxValueQuery>
                <CmdletParameterMetadata PSName="MaxWaitHint" />
              </MaxValueQuery>
            </Property>
          </QueryableProperties>
        </GetCmdletParameters>
      </GetCmdlet>

      <!--Get-ServiceSecurityDescriptor: invoking method GetSecurityDescriptor():-->
      <Cmdlet>
        <!--defining the ConfirmImpact which indicates how severe the changes are that this cmdlet performs-->
        <CmdletMetadata Verb="Get" Noun="WmiServiceSecurityDescriptor" ConfirmImpact="Low" />
        <!--defining the WMI instance method used by this cmdlet:-->
        <Method MethodName="GetSecurityDescriptor">
          <ReturnValue>
            <Type PSType="system.uint32" />
            <CmdletOutputMetadata>
              <ErrorCode />
            </CmdletOutputMetadata>
          </ReturnValue>
          <!--defining the parameters of this cmdlet:-->
          <Parameters>
            <!--Method returns an instance of Win32_SecurityDescriptor in property Descriptor-->
            <Parameter ParameterName="Descriptor">
              <Type PSType="Microsoft.Management.Infrastructure.CimInstance[]" />
              <CmdletOutputMetadata />
            </Parameter>
          </Parameters>
        </Method>
      </Cmdlet>

    </InstanceCmdlets>
  </Class>
</PowerShellMetadata>
'@

# create a test folder if it does not exist
$exists = Test-Path -Path $testfolder -PathType Container
if (!$exists) { $null = New-Item -Path $testfolder -ItemType Directory }

# write cdxml file
$path = Join-Path -Path $testfolder -ChildPath 'win32_service.cdxml'
$cdxml | Set-Content -Path $path -Encoding UTF8

# import the module
Import-Module -Name $path -Verbose -Force

Using The New Cmdlets

Once you run this, the newly created module Win32_Service now provides you with two super-useful new cmdlets:

Get-WmiService returns services to you, and you have a plethora of query parameters that you can use to find exactly the services you need. All query parameters automatically compose a server-side WMI query that is much more efficient than using Where-Object.

Get-WmiServiceSecurityDescriptor returns the security descriptor of one or more services. You can now run it stand-alone like this:

Get-WmiServiceSecurityDescriptor -Name spooler, wuauserv

The result looks like this:

ControlFlags   : 32788
DACL           : {Win32_ACE, Win32_ACE, Win32_ACE}
Group          : Win32_Trustee
Owner          : Win32_Trustee
SACL           : {Win32_ACE}
TIME_CREATED   :
PSComputerName :

ControlFlags   : 32788
DACL           : {Win32_ACE, Win32_ACE, Win32_ACE}
Group          : Win32_Trustee
Owner          : Win32_Trustee
SACL           : {Win32_ACE}
TIME_CREATED   :
PSComputerName :

Plus, all CDXML-defined cmdlets come with rich and powerful remoting capabilities.

What’s Next

In this article I focused only on key parts of CDXML-generated PowerShell modules. Ultimately, I’ll publish a free module soon that wraps all classic WMI classes and produces hundreds of useful cmdlets.

Until we get there, there’s still some ground to cover. In the next part, I’ll look at static methods that you can use to create new things.