I recently needed to solve a specific problem: track Group Policy Objects (GPOs) that had been deleted from Active Directory. The key requirement was to identify GPOs that had valid names before deletion - this helps when someone accidentally deletes a GPO and needs to know what it was called and when it was deleted.
When GPOs are deleted in Active Directory, they end up in the AD Recycle Bin. While you can query these objects, I needed a solution that would:
- Only track GPOs with valid display names (not interested in unnamed objects)
- Send daily notifications about newly deleted GPOs
- Avoid sending the same information repeatedly
- Provide flexibility for different reporting needs
This is the first option where all the deleted GPO's are inline which means they are part of the email:
I developed two PowerShell scripts to handle this requirement, each serving different use cases.
Approach 1: HTML Email with Inline Details
The first script sends a professional HTML email containing the GPO details directly in the message body. This is ideal for quick visual scanning by administrators.
# Get deleted GPOs that have a display name
$deletedGPOs = Get-ADObject -Filter {objectClass -eq "groupPolicyContainer" -and IsDeleted -eq $true} `
-IncludeDeletedObjects `
-Properties displayName, gPCFileSysPath, whenCreated, whenChanged, lastKnownParent, msDS-LastKnownRDN, ObjectGUID |
Where-Object { $_.displayName -ne $null -and $_.displayName -ne "" }
The key here is filtering for GPOs where displayName is not null or empty - I was only interested in GPOs that had actual names.
Approach 2: CSV Attachment for SIEM Integration
The second script generates a CSV file and attaches it to the email. This format is perfect for ingesting into a SIEM system or other automated processing:
# Create CSV with specific format: Date,guid,gponame
$csvData = $gposToEmail | ForEach-Object {
[PSCustomObject]@{
Date = $_.WhenDeleted.ToString('yyyy-MM-dd HH:mm:ss')
guid = $_.ObjectGUID.ToString()
gponame = $_.GPOName
}
}
Preventing Duplicate Notifications
I didn't want to receive emails containing the same GPO information day after day. To solve this, I implemented a JSON-based tracking system:
# Get previously tracked deleted GPOs
if (Test-Path $JsonFilePath) {
$previouslyTracked = Get-Content $JsonFilePath | ConvertFrom-Json
$trackedGUIDs = $previouslyTracked | ForEach-Object { $_.ObjectGUID }
} else {
$previouslyTracked = @()
$trackedGUIDs = @()
}
# Find newly deleted GPOs
$newlyDeletedGPOs = $allDeletedGPOs | Where-Object { $_.ObjectGUID -notin $trackedGUIDs }
This means:
- First run: The script records all currently deleted GPOs in the JSON file but doesn't send an email
- Subsequent runs: Only newly deleted GPOs (not in the JSON file) trigger email notifications
The Full Report Option
Sometimes you need a complete picture of all deleted GPOs, regardless of what's been reported before. I added a -Fullparameter that bypasses the tracking check:
[CmdletBinding()]
param(
[switch]$Full
)
# Determine which GPOs to email about
if ($Full) {
$gposToEmail = $allDeletedGPOs
} else {
$gposToEmail = $allDeletedGPOs | Where-Object { $_.ObjectGUID -notin $trackedGUIDs }
}
Running .\DeletedGPOReport.ps1 -Full sends a complete report without updating the tracking file, preserving the normal incremental behavior for scheduled runs.
Key Implementation Details
Extracting Meaningful Data
The script extracts several important attributes from deleted GPOs:
[PSCustomObject]@{
GPOName = $_.displayName
ObjectGUID = $_.ObjectGUID
WhenCreated = $_.whenCreated
WhenDeleted = $_.whenChanged # This represents deletion time
LastParent = $_.lastKnownParent
}
Scheduling for Daily Runs
I set up a scheduled task to run the script daily:
schtasks /create /tn "Daily GPO Deletion Report" /tr "powershell.exe -ExecutionPolicy Bypass -File C:\Scripts\DeletedGPOReport.ps1" /sc daily /st 08:00Important Considerations
I focused specifically on GPOs with valid names because unnamed GPO objects in the recycle bin are typically not useful for audit purposes. The script deliberately filters these out to keep reports clean and actionable.
The JSON tracking file serves as a persistent state store, ensuring the solution works correctly even if the script execution is interrupted or the server is restarted.
Conclusion
This solution provides a robust way to track deleted GPOs without overwhelming administrators with repetitive information. Whether you prefer inline HTML reports for quick review or CSV attachments for automated processing, the scripts can be adapted to your specific needs.
The combination of incremental reporting with the option for full reports gives the flexibility needed for both daily operations and periodic comprehensive reviews.