Thursday, February 11, 2010

Using Powershell to search XML GPO Reports – Part II – Software Installation

In Part I of our series in querying group policy reports, we learned how to:
  • Turn the XML output from Get-GPOReport into a System.XML.XMLDocument object
  • Build a System.XML.XMLNamespaceManager from the information in the XMLDocument object
  • Use XPath with the namespace manager and document to extract information from the report, including the Extension elements.
We left off with the Get-CGPOReportExtensionData filter, and some questions about it’s output.  This week we’ll look at the output from the filter, and then use that output in another filter to transform the XML report information into an object with the details of a Software Installation extension that can easily be worked with using Powershell’s standard comparison operators and formatting cmdlets.

First, let’s examine the output of Get-CGPOReportExtensionData.  When output to the console it looks like:


fullReport             : #document
extensionNamespaceURI  :
http://www.microsoft.com/GroupPolicy/Settings/SoftwareInstallation
namespaceMgr           : {e, q1, , xml...}
extensionNamespaceName : q1
extensionData          : Extension
GPOName                : apps-Publisher


Lets go through what each of these are.
  • ‘FullReport’ is the XMLDocument object built from the XML output of Get-GPOReport that we found matching extension information in.  This is useful in the event that we want to extract information about the group policy object itself. 
  • ‘ExtensionNamespaceURI’ is the URI for the namespace that we need in order to use XPath queries to extract information from the extension element. 
  • ‘ExtensionNamespaceName’ is the namespace name for the same thing.  The namespace name is important as we have to use it in the XPath queries we build later on, the URI somewhat less so but I included it just for the sake of the completion compulsion I had at the time I wrote it. 
  • ‘NamespaceMgr’ is an XMLNamespaceManager object that, because there are namespace declarations in the XMLDocument, is necessary to be able to use the methods of the XML classes that accept XPath queries.
  • ‘ExtensionData’ is the extension XMLNode that matched the ‘-extensionName’ parameter of Get-CGPOReportExtensionData.  It will prove quite useful to use in our XPath queries later.
  • ‘GPOName’ is the name of the group policy object that contains the matching extension data.  Pretty straightforward stuff.
The only information we didn’t cover generating in the last part of this series is the extensionNamespaceURI and extensionNamespaceName, so let’s quickly cover that.  The group policy folks were kind enough to make the namespace URI suffix for each extension the same as the extension’s name, without any spaces.  So generating that from the extension name that we’re looking for is pretty straightforward:

    $extensionURISuffix = $extensionName -replace " "

Armed with that information we can generate the URI and use a method of the XMLElement, ‘GetPrefixOfNamespace’, to get the namespace prefix…which I now realize I have been calling the namespace name.  Oops, gonna have to fix that. Anyway:

    # Get the extension specific namespace name
    $extensionNamespaceURI  = "
http://www.microsoft.com/GroupPolicy/Settings/$extensionURISuffix"
    $extensionNamespaceName = $extension.GetPrefixOfNamespace($extensionNamespaceURI)


And that’s about it for the basic information about the group policy object down to the extension.  So now let’s dig into our first extension, Software Installation, in some detail.  Don’t forget to download the updated module code and save it as a module (<moduleFolder>\clint.gputilities\clint.gputilities.psm1), and to import it into your Powershell session (Import-Module clint.gputilities) as well as importing the grouppolicy module.  First, let’s get some output from our first filter to work with:

PS > $swiExtensions = Get-GPOReport –All –ReportType XML | Get-CGPOReportExtensionData –ExtensionName “Software Installation”
PS > $e = $swiExtensions[0]


At this point we should have an object, $e, that we can dig into.  But to dig into it we need the namespace manager, how handy that it’s right there for the picking:

PS > $nsMgr = $e.namespaceMgr

Yeah, we could just use $e.namespaceMgr all the time but that’s kind of a pain.  Next we need the namespace prefix in order to use XPath to dig into the extension information.  I promise I’ll fix the variable name by next part in the series…

PS > $ns = $e.extensionNamespaceName

Alright, now we can start digging.  Each software installation element can contain multiple package child elements.  We only use MSI packages here, so that’s what we’ll be looking at, but if someone has an XML report for one of the few other types of installation packages I’d be glad to incorporate those.  Let’s gather the MsiApplication nodeset:

PS > $msiNodes = $e.extensionData.SelectNodes(“./$ns`:MsiApplication”,$nsMgr)

There are a few things to note here.  First, that we finally put the namespace prefix ($ns) from our extension data to use.  Second, that we have to use the Powershell escape character ‘`’ before the colon following the namespace prefix variable.  Why?  Because of the way variables can be addressed.  For instance, when we want to check the username environment variable we use:

PS > $env:username

The ‘:’ says, “Go to the env psdrive, and select the username childitem”.  So the statement is generally ‘$PSDrive:ChildItem’.  So then this has implications when we use variable expansion inside of a string.  Continuing the username environment variable example, if we wanted to include it inside of a string we can use multiple methods:

PS > “My username is:  “ + $env:username
PS > “My username is:  {0}” –f $env:username
PS > “My username is:  $env:username”


The same string will be produced in all three cases, “My username is: clint”.  See the potential problem with our XPath string?  We need Powershell to recognize that the namespace prefix is followed by a literal colon, otherwise it’s going to go looking for a psdrive with the name of our namespace prefix, and probably come up with nothing.  So we use the ‘`’ escape character to tell Powershell the colon is literal and our XPath queries work.  That diversion aside, next we’ll select the first child node in the event that there are more than one MsiApplication elements:

PS > $m = $msiNodes.item(0)

Now let’s look at how $m is presented by the console’s output:

PS > $m

Identifier          : {297a8ec5-2e10-49ed-931e-90d7cef549cb}
Name                : Microsoft Publisher 2002
Path                : \\server\share\MS Publisher 2002\PUB.MSI
MajorVersion        : 10
MinorVersion        : 0
LanguageId          : 1033
Architecture        : 0


Hey, easy from here, we’re done…right?  Not quite.  Yes, the way Powershell surfaces XML objects can make them a bit easier to work with, but it does have its flaws and starts to break down as noted in Processing XML with PowerShell.  So let’s use the XML object itself to create a custom PSObject that fits our needs and that we can re-use down the pipe.  There are essentially two types of XMLNodes in $m, those that only contain a single XmlText node, and those that contain other elements.  The elements that only contain a single XmlText node are the easiest to create a property from.  Let’s take ‘Identifier’ as an example:

PS > $m.SelectSingleNode("./$ns`:Identifier",$nsMgr).InnerText
{297a8ec5-2e10-49ed-931e-90d7cef549cb}


That’s not too tough.  Which is good, because there’s a lot of those nodes in the MsiApplication node.  The nodes that contain other element nodes are more complicated to turn into properties directly, most notably the SecurityDescriptor element (which we won’t tackle yet), but this can be accomplished using script blocks.  Let’s take ‘Upgrades’ as our example this time.  The XML itself it looks like:

<q2:Upgrades>
  <q2:Mandatory>true</q2:Mandatory>
  <q2:PackageInfo>
    <q2:GPOIdentifier>
      <Identifier xmlns="
http://www.microsoft.com/GroupPolicy/Types">{7A2F01E5-895E-4029-AC3D-9F478DB4DEA7}</Identifier>
      <Domain xmlns="
domain.comhttp://www.microsoft.com/GroupPolicy/Types">domain.com</Domain>
    </q2:GPOIdentifier>
    <q2:GPOName>adm-WorkStations</q2:GPOName>
    <q2:Name>Microsoft Office XP Professional</q2:Name>
    <q2:Identifier>{45b72218-66bf-41e0-beeb-881eab80dd31}</q2:Identifier>
  </q2:PackageInfo>
</q2:Upgrades>


So here the ‘Mandatory’ element should be simple enough to extract, in fact in our module I decided to break it out into it’s own property, UpgradesMandatory.  To get at it is fairly straightforward:

PS > $m.SelectSingleNode("./$ns`:Upgrades/$ns`:Mandatory",$nsMgr).InnerText

However, there can be multiple ‘PackageInfo’ elements under ‘Upgrades’, so we need a little more power to turn that into a property that’s complete and easy to work with.  I chose to create a PSObject array and add properties to each object that reflect the data contained in GPOIdentifier, GPOName, Name, and Identifier.  Though the following code isn’t complete yet, it will be soon and gets the idea across:

[PSObject[]]$arrUpgrades = @()
foreach ($p in $m.SelectNodes("./$ns`:Upgrades/$ns`:PackageInfo",$namespaceMgr))
{
    $arrUpgrades += New-Object PSObject -Property @{
        GPOIdentifier = $null #TODO: Get GPOIdentifier
        GPODomain            = $null #TODO: Get GPODomain
        GPOName                = $p.SelectSingleNode("./$ns`:GPOName",$namespaceMgr).InnerText
        Name                = $p.SelectSingleNode("./$ns`:Name",$namespaceMgr).InnerText
        Identifier    = $p.SelectSingleNode("./$ns`:Identifier",$namespaceMgr).InnerText
    }
}
Write-Output $arrUpgrades


We can follow a similar pattern to create objects from other complex nodes.  So, big deal, what can we do now?  Just had a use for this yesterday, in fact.  We’re working on migrating most of our software distribution from Group Policy to System Center Configuration Manager and needed to know what software was being installed through Group Policy.  So using this module we were easily able to produce that info with:

PS > Get-CGPOReportSoftwareInstallationData | Select-Object name,majorversion,minorversion –unique | Format-Table name,majorversion,minorversion –auto

Name                                        MajorVersion MinorVersion
----                                        ------------ ------------
Microsoft Publisher 2002                    10           0
Microsoft Office XP Professional            10           0
Microsoft Office Professional Edition 2003  11           0
Sun Java Runtime Environment                1            4


Now that I’ve been working on this little project for a few weeks off and on, I’m thinking about reworking the filters a bit for workflow reasons.  So the next time we see this module it will be a little bit different, but all the guts of working with the XML remain the same.  The next entry in the series we will at least complete extracting the SecurityDescriptor information, but I’m sure I’ll get more work done on this than that.  The current version of the module is below.

 Download clint.gputilities.psm1