Friday, January 29, 2010

Using PowerShell to search Group Policy XML Reports

I was going to write about creating, editing, and saving XML files using the System.XML objects today but discovered it had been covered quite well already after a quick Google search.  So instead we’ll jump ahead a little bit and use PowerShell & .NET’s XML capabilities to dive into the GPO Reports that can be generated using Get-GPOReport from the grouppolicy module.

This will be the first in a small series of posts that should end up with us having a module capable of searching for any group policy setting that is recorded in the XML reports.  That’s the goal, but I’m posting as I get things 90% complete so hopefully all this proves useful and you’ll be willing to bear with me.

Before that, if you need a little XML for PowerShell primer, I suggest giving these posts a read first:

Processing XML with PowerShell
Processing XML with PowerShell II
XML Part 1: Playing with RSS Feeds and XML Content
XML Part 2: Write, Add And Change XML Data

With the basics out of the way, onto the fun!  Well, OK, not quite yet.  First the requirements:
  • Windows 7 or Server 2008 R2 with the RSAT installed
That was easy.  The first thing we’ll need to do is import the grouppolicy module.

PS C:\> import-module grouppolicy

But, without a group policy report to work with we’re not much of anywhere so let’s get a collection of all our reports ( probably not a good idea if you have thousands of GPOs, you’ll have to fend for yourselves ), then pick the first one to inspect.

PS C:\> $allGPOReports = Get-GPOReport –All –ReportType XML
PS C:\> $GPOReport = $allGPOReports[0]

If we inspect $GPOReport through it’s GetType() method we’ll see that it’s just a string at the moment.

PS C:\> $gporeport.gettype() | ft -auto

IsPublic IsSerial Name   BaseType    
-------- -------- ----   --------    
True     True     String System.Object


It’s a really long string, but a string nonetheless.  Let’s do a couple of things to get a good look at it.  First we’ll dump it to a file so we can open it in our favorite XML viewer/editor.  Then we’ll create a System.XML.XMLDocument object to work with in PowerShell.

PS C:\> out-file -InputObject $GPOReport -FilePath C:\Temp\gporeport.xml
PS C:\> $xmlReport = New-Object system.xml.xmldocument
PS C:\> $xmlReport.LoadXml($GPOReport)

I’ve been working with a few different XML editors lately and I think I’ve settled on <oXygen/> XML Editor but we’re still in our trial period, so we’ll see.  If you don’t have access to an XML IDE, then something like NotePad++ is a great alternative.  Anything with syntax highlighting and code folding for XML will prove handy, however.  Once you have the gporeport.xml file open, let’s look at the second line:

<GPO xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.microsoft.com/GroupPolicy/Settings">

We’re going to need those namespace declarations in order to query our report for anything, so lets setup a namespace manager with the proper information. First we’ll get the collection of namespaces declared in the line above, then populate the namespace manager with those.

PS C:\> $nsMgr = New-Object System.Xml.XmlNamespaceManager $xmlReport.CreateNavigator().NameTable
PS C:\> $namespaces = $xmlReport.CreateNavigator().GetNamespacesInScope('All')
PS C:\> foreach ($key in $namespaces.keys) { $nsMgr.AddNamespace( $key, $namespaces.$key ) }
PS C:\> $nsmgr.AddNamespace( "e", $nsmgr.DefaultNamespace )

Why that last line?  The short of it is we have to prefix anything in the XML document in the default namespace with a named namespace in order for .NET to find it.  The long version is in the MSDN docs.  I call mine “e” because it’s easy to type.  You can pick whatever you want as long as it doesn’t collide with other namespaces in the document.  Okay, so now we have a System.Xml.XMLDocument object to search, and most of the namespaces we need.  Most?  Yep, most.  But we’ll tackle that later.  For now, let’s see what we get looking for an extension.

PS C:\> $xmlReport.SelectNodes( "//e:ExtensionData", $nsMgr ) | ft -auto

Extension Name              
--------- ----              
Extension Security          
Extension Public Key        
Extension Registry          
Extension Remote Installation


Hey!  We got results, awesome!  Yours probably look different than mine, but that’s OK.  So now with just a little work and a little XPATH know-how, we can look inside of our GPO reports for extensions, and a whole lot more.  Last Modification Time when the report was run?

PS C:\> [datetime]($xmlReport.SelectSingleNode( "/e:GPO/e:ModifiedTime", $nsMgr ).'#text')

Friday, September 04, 2009 4:16:13 PM

Where was this GPO linked to when this report was generated?

PS C:\> $xmlReport.SelectNodes( "/e:GPO//e:LinksTo", $nsMgr ) | ft -auto

SOMName      SOMPath                                             Enabled NoOverride
-------      -------                                             ------- ----------
Aaron-PC     domain.org/00-hardware/WorkStations/ADM/ws/Aaron-PC true    false    
projectorPCs domain.org/00-hardware/WorkStations/projectorPCs    true    false    
psd267       domain.org                                          true    false


Which of our GPO reports have configuration for the “Files” extension?

PS C:\> $allGPOReports | Get-CGPOReportExtensionData -extensionName "Files"

fullReport             : #document
extensionNamespaceURI  :
http://www.microsoft.com/GroupPolicy/Settings/Files
extensionNamespaceName : q4
extensionData          : Extension
GPOName                : DriveMaps_Files_Folders_Shortcuts


fullReport             : #document
extensionNamespaceURI  :
http://www.microsoft.com/GroupPolicy/Settings/Files
extensionNamespaceName : q1
extensionData          : Extension
GPOName                : apps-Deploy-Audacity


Hey, what the, where did “Get-CGPOReportExtensionData” come from?  And what’s with the q1/q4 namespace junk?  Get-CGPOReportExtensionData is the first function in our module we’re working on.  No doubt it will evolve over the next few weeks as we snorkel around (I leave the deep diving to the pros and MVPs) in the XML reports, in fact I found a better way to extract namespace info from the extensions while I wrote this post, though I haven't updated the module with it yet.   The code for the module is below.  Just save it as "clint.gputilities.psm1" in one of your module folders in a subdirectory called “clint.gputilities” and then import it into your PowerShell session to start using it.  Hopefully we can come up with a better name by the time we’re done.

PS C:\> Import-Module clint.gputilities

Next week we’ll refine this filter a bit, and dig into our first extension: Software Installation.  See you then!

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
filter Get-CGPOReportExtensionData
{
<# .SYNOPSIS 
  Queries XML Reports generated by Get-GPOReport for specific Extension information 
.DESCRIPTION
  Finds the extension specified by the parameter ExtensionName in the Report or Reports
  (i.e. Files, Registry, Software Installation). Tacks on the namespace information
  necessary to query the extension onto the report as a custom PSObject and writes that 
  information to output.
.PARAMETER gpoReport
  A report generated by Get-GPOReport -reportType XML.
.PARAMETER namespaceMgr
  If no namespace manager is assigned to this value, the default namespaces for group 
  policy XML reports are used.
.PARAMETER extensionName
  The name of the group policy extension that you wish to find in the reports.
  Valid names I am currently aware of:
    Security,Public Key,Registry,Remote Installation,Internet Explorer Maintenance,
    Software Installation,Scripts,Folder Redirection,Printers,Windows Firewall,
    Software Restriction,Drive Maps,Shortcuts,Folders,Files,Windows Registry,
    Environment Variables,WLanSvc Networks,Folder Options,Start Menu,
    Deployed Printer Connections Policy,Ini Files
.EXAMPLE
  Get-GPOReport -All -ReportType XML | Get-CGPOReportExtension Data -ExtensionName "Drive Maps"
.INPUTS
  [System.XML.XMLDocument]
.OUTPUTS
  [System.Management.Automation.PSCustomObject]
.NOTES
  Todo.
.FUNCTIONALITY
  Todo.
#>
  #region cmdletbinding
  [CmdletBinding()]
  #endregion
  #region parameters
  param
  (
    [parameter(
      Mandatory=$true,
      ValueFromPipeline=$true
      )]
    [Xml.XmlDocument]
    $gpoReport,
    [parameter(
      Mandatory=$false
      )]
    [Xml.XmlNamespaceManager]
    $namespaceMgr,
    [parameter(
      Mandatory=$true
      )]
    [String]
    $extensionName
  )
  #endregion
  process
  {
   # Build a namespace manager if we don't have one
if (-not $namespaceMgr)
{
# Create a namespace manager from our navigator object's nametable
$namespaceMgr = New-Object System.Xml.XmlNamespaceManager $gpoReport.CreateNavigator().NameTable
$namespaceMgr.AddNamespace( "e", "http://www.microsoft.com/GroupPolicy/Settings" )
}
    # We're going to cheat and use Posh's dotted notation to get the GPO name
    # there is only one GPO node (it's the root), and only one Name element
    $GPOName = $gpoReport.GPO.Name

    # Gather the Extensions that match the our queryCSE
    $extensions = $gpoReport.selectnodes("/e:GPO//e:ExtensionData[e:Name = '$extensionName']/e:Extension", $namespaceMgr)
    foreach ($extension in $extensions)
    {
      # Init the extension specific namespace variable
      $extensionNamespaceName = $null
      $extensionNamespaceURI  = $null
     # We need the Extension child element of this ExtensionData element
     # and have to extract the specific namespaces assigned for Extension
     # by the 'Get-GPOReport' cmdlet
     $eNavigator = $extension.CreateNavigator()
     $eNamespace = $eNavigator.GetNamespacesInScope('All')
     foreach ($key in $eNamespace.keys)
     {
       # The namespace assignments we're looking for are always named 'q1', 'q2', ... 'q99'
       # If there's more than a hundred, someone needs to start splitting up their GPOs, :)
       if ($key -match '^q\d{1,2}$')
       {
       # Now we have the namespace assignment that is valid to query this element
       $gpoExtInfo = New-Object PSObject -Property @{
       GPOName = $GPOName
       fullReport = $gpoReport
       extensionData  = $extension
       extensionNamespaceName  = $key
       extensionNamespaceURI   = $eNamespace.$key
        }
       }
      }
      Write-Output $gpoExtInfo
    }
  }
}
Export-ModuleMember Get-CGPOReportExtensionData