Powershell : Monitor GPO's Updates (with history)

I recently did a post on this before which you can view here about monitoring Group Policy objects, however its always a good idea to improve on your work as you go as this is version 2 of that script, that is more technical but is very helpful in pinpointing events.

Note : This will take events directly from the Domain Controllers which means a valid entry is present on the Domain Controller for this report to exist, which means this is only as valid as the "scope " of your security log before it is overridden.

The previous script did have no cavets and I am sure this one will with time, but in testing all has been well and the results are very informative, this is how the plan looks and what the script does:

  1. Define the XML for the Event Log query
  2. Get a list of GPO GUID's 
  3. Add the DisplayName of the GPO to a field in the CSV so its human readable
  4. Cycle though all the Domain Controllers and look for the Event ID
  5. Map the relevant fields to the variables names in the script
  6. Output this to a CSV file

First we need to get an example alert which will come from the Security log and we are looking for the Event ID of 5136 which translates as "A directory service object was modified" however there are hundreds of these but we are interested in a certain type of alert, which is driven by the XML:

<QueryList>
  <Query Id="0" Path="Security">
    <Select Path="Security">
      *[System[(EventID=5136) and TimeCreated[timediff(@SystemTime) &lt;= 86400000]]]
      and
      *[EventData[Data[@Name='ObjectClass'] and (Data='groupPolicyContainer')]]
      and
      *[EventData[Data[@Name='AttributeLDAPDisplayName'] and (Data='versionNumber')]]
      and
      *[EventData[Data[@Name='OperationType'] and (Data='%%14674')]]
    </Select>
  </Query>
</QueryList>

This will give you events like this that are only relevant to the task at hand, we then need to extract data from this event which will be the script variables these are in bold below:

A directory service object was modified.

Subject:
    Security ID:        bear.local\GPO.Editor
    Account Name:        gpo.editor
    Account Domain:        BEAR
    Logon ID:        0x8EAA37C51

Directory Service:
    Name:    bear.local
    Type:    Active Directory Domain Services

Object:
    DN:    CN={3E84CDDE-5EEA2-4381-B566-C36AAE9BF012},CN=POLICIES,CN=SYSTEM,DC=BEAR,DC=LOCAL
    GUID:    CN={3E84CDDE-5EEA2-4381-B566-C36AAE9BF012},CN=Policies,CN=System,DC=bear,DC=local
    Class:    groupPolicyContainer

Attribute:
    LDAP Display Name:    versionNumber
    Syntax (OID):    2.5.5.9
    Value:    262146

Operation:
    Type:    Value Added
    Correlation ID:    {44ecdea7-cb38-4eb7-886d-881721cf9c98}
    Application Correlation ID:    -

This has obviously given us the GUID of  "3E84CDDE-5EEA2-4381-B566-C36AAE9BF012" which is not very helpful for humans to read, however we can use this command which will then give us the details:

Get-GPO -Guid CN=3E84CDDE-5EEA2-4381-B566-C36AAE9BF012

This will then tell us the following information from which we require the DisplayName attribute:

DisplayName      : Disable Edge (Chrome is better)
DomainName       : bear.local
Owner            : BEAR\gpo.manager
Id               : 3e84cdde-5dd4-4385-b565-c36aae9bf012
GpoStatus        : AllSettingsEnabled
Description      : He who shall not be named!
CreationTime     : 04/09/2024 17:15:06
ModificationTime : 17/09/2024 14:11:54
UserVersion      : AD Version: 4, SysVol Version: 4
ComputerVersion  : AD Version: 3, SysVol Version: 3

We now have all the information to get this CSV file correct, so lets get some code to get all that done with one script which will track not only what has been changed but how many times it has been changed with the version increments.

Script : GPO-XMLInterrogate.ps1

# Import required modules

Import-Module ActiveDirectory
Import-Module GroupPolicy

# XML query with time limitation (adjust as needed)
$xmlQuery = @"
<QueryList>
  <Query Id="0" Path="Security">
    <Select Path="Security">
      *[System[(EventID=5136) and TimeCreated[timediff(@SystemTime) &lt;= 86400000]]]
      and
      *[EventData[Data[@Name='ObjectClass'] and (Data='groupPolicyContainer')]]
      and
      *[EventData[Data[@Name='AttributeLDAPDisplayName'] and (Data='versionNumber')]]
      and
      *[EventData[Data[@Name='OperationType'] and (Data='%%14674')]]
    </Select>
  </Query>
</QueryList>
"@

# Function to resolve GUID to GPO display name
function Get-GPODisplayName {
    param($guidString)
    if ([string]::IsNullOrWhiteSpace($guidString)) {
        Write-Warning "Empty GUID string provided"
        return "Unknown GPO (Empty GUID)"
    }
    try {
        $gpo = Get-GPO -Guid $guidString -ErrorAction Stop
        return $gpo.DisplayName
    } catch {
        $errorMessage = $_.Exception.Message
        Write-Warning "Error retrieving GPO for GUID $guidString : $errorMessage"
        return $null
    }
}

# Get all domain controllers
$domainControllers = Get-ADDomainController -Filter * | Select-Object -ExpandProperty Hostname

# Array to store all events
$allEvents = @()

# Query each domain controller
foreach ($dc in $domainControllers) {
    Write-Host "Querying $dc..."
    $events = Get-WinEvent -FilterXml $xmlQuery -ComputerName $dc -ErrorAction SilentlyContinue
    if ($events) {
        $allEvents += $events | ForEach-Object {
            $eventXML = [xml]$_.ToXml()
             $objectDN = $eventXML.Event.EventData.Data | Where-Object { $_.Name -eq 'ObjectDN' } | Select-Object -ExpandProperty '#text'
        $guidString = $objectDN -replace 'CN=\{(.*?)\},.*', '$1'   
            $subjectUserName = $eventXML.Event.EventData.Data | Where-Object { $_.Name -eq 'SubjectUserName' } | Select-Object -ExpandProperty '#text'
            $subjectDomainName = $eventXML.Event.EventData.Data | Where-Object { $_.Name -eq 'SubjectDomainName' } | Select-Object -ExpandProperty '#text'
                       $versionNumber = $eventXML.Event.EventData.Data | Where-Object { $_.Name -eq 'AttributeValue' } | Select-Object -ExpandProperty '#text'       

            $eventData = @{
                TimeCreated = $_.TimeCreated
                DC = $dc
                SubjectUserName = "$subjectDomainName\$subjectUserName"
                GUID = $guidString
                DisplayName = (Get-GPODisplayName $guidString)
                VersionNumber = $versionNumber
            }
            New-Object PSObject -Property $eventData
        }
    }
}

# Export to CSV
$csvPath = "GPOChanges.csv"
$allEvents | Select-Object TimeCreated, DC, SubjectUserName, GUID, DisplayName, VersionNumber | Export-Csv -Path $csvPath -NoTypeInformation

Write-Host "Report has been exported to $csvPath"

This will the save a CSV file that will be formatted in a very easy to read format for technical people to understand which will then as you can see track all the version updates and relevant times.


This clearly shows you that the GPO policy named "Disable Edge (Chrome is better) was updated 3 times as per the TimeCreated timestamp and that the update at 16:39 was the most up to date version due to the VersionNumber.

Previous Post Next Post

نموذج الاتصال