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
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 |