So, we have the script to report on who has amended which GPO, but this does not cover who as updated links within the GPO objects, these "links" are how you link a GPO to a an OU like this:
Then you have the other end of that equation where you get the option to turn off the link or enforce, not to mention deleting the link as well:
Mission : Scripting requirements
What we need here is a process that works like this:- Capture the baseline of the status (in JSON format)
- Capture a "new copy" of the live data (in JSON format)
- Compare the differences
- Produce a report of the changes (in JSON format)
- Optional : Update the Baseline after changes detected
The script for that process
# Load the GroupPolicy module
Import-Module GroupPolicy
# Function to capture the current state of GPO links
function Get-GPOLinks {
$allGpos = Get-GPO -All
$gpoLinks = @()
foreach ($gpo in $allGpos) {
$report = Get-GPOReport -Guid $gpo.Id -ReportType Xml
[xml]$reportXml = $report
if ($reportXml.GPO.LinksTo.Link) {
foreach ($link in $reportXml.GPO.LinksTo.Link) {
$gpoLinks += [PSCustomObject]@{
GpoId = $gpo.Id
GpoName = $gpo.DisplayName
Domain = $gpo.DomainName
LinkPath = $link.SOMPath
LinkEnabled = $link.Enabled
LinkEnforced = $link.Enforced
}
}
}
}
return $gpoLinks
}
# Function to save the state to a JSON file
function Save-StateToFile {
param (
[Parameter(Mandatory = $true)]
[string]$FilePath,
[Parameter(Mandatory = $true)]
[array]$State
)
$State | ConvertTo-Json -Depth 10 | Set-Content -Path $FilePath
}
# Function to load the state from a JSON file
function Load-StateFromFile {
param (
[Parameter(Mandatory = $true)]
[string]$FilePath
)
if (Test-Path $FilePath) {
return Get-Content -Path $FilePath | ConvertFrom-Json
} else {
return @()
}
}
# Function to get a hash of an object for comparison
function Get-ObjectHash {
param (
[Parameter(Mandatory = $true)]
[object]$Object
)
$jsonString = $Object | ConvertTo-Json -Depth 10
$bytes = [System.Text.Encoding]::UTF8.GetBytes($jsonString)
$hash = [System.Security.Cryptography.MD5]::Create().ComputeHash($bytes)
return [BitConverter]::ToString($hash) -replace '-', ''
}
# Function to compare two states and generate a report
function Compare-States {
param (
[Parameter(Mandatory = $true)]
[array]$Baseline,
[Parameter(Mandatory = $true)]
[array]$Current
)
$baselineHash = @{}
foreach ($item in $Baseline) {
$key = "$($item.GpoId):$($item.LinkPath)"
$baselineHash[$key] = Get-ObjectHash -Object $item
}
$currentHash = @{}
foreach ($item in $Current) {
$key = "$($item.GpoId):$($item.LinkPath)"
$currentHash[$key] = Get-ObjectHash -Object $item
}
$added = @()
$removed = @()
$modified = @()
foreach ($key in $currentHash.Keys) {
if (-not $baselineHash.ContainsKey($key)) {
$added += $Current | Where-Object { "$($_.GpoId):$($_.LinkPath)" -eq $key }
} elseif ($baselineHash[$key] -ne $currentHash[$key]) {
$modified += [PSCustomObject]@{
Baseline = $Baseline | Where-Object { "$($_.GpoId):$($_.LinkPath)" -eq $key }
Current = $Current | Where-Object { "$($_.GpoId):$($_.LinkPath)" -eq $key }
}
}
}
foreach ($key in $baselineHash.Keys) {
if (-not $currentHash.ContainsKey($key)) {
$removed += $Baseline | Where-Object { "$($_.GpoId):$($_.LinkPath)" -eq $key }
}
}
return [PSCustomObject]@{
Added = $added
Removed = $removed
Modified = $modified
}
}
# Define file paths
$baselineFilePath = "Baseline.json"
$currentFilePath = "Current.json"
$reportFilePath = "Report.json"
# Capture the baseline state if it does not exist
if (-not (Test-Path $baselineFilePath)) {
Write-Output "Capturing baseline state..."
$baselineState = Get-GPOLinks
Save-StateToFile -FilePath $baselineFilePath -State $baselineState
Write-Output "Baseline state saved to $baselineFilePath"
} else {
Write-Output "Loading existing baseline state..."
$baselineState = Load-StateFromFile -FilePath $baselineFilePath
}
# Capture the current state
Write-Output "Capturing current state..."
$currentState = Get-GPOLinks
Save-StateToFile -FilePath $currentFilePath -State $currentState
Write-Output "Current state saved to $currentFilePath"
# Compare the baseline and current states
Write-Output "Comparing baseline and current states..."
$report = Compare-States -Baseline $baselineState -Current $currentState
# Save the comparison report to a JSON file
Write-Output "Saving report..."
$report | ConvertTo-Json -Depth 10 | Set-Content -Path $reportFilePath
Write-Output "Report saved to $reportFilePath"
# Output the report to the console
Write-Output "Report:"
$report | Format-List
When you run the script it will tell you what stage it is on and the give you a quick summary as a report, you can see this below:
The files Created and why?
These are the files you will get created, obviously the GPO-LinkTracker is the main script to run, then you have the Baseline (for the reference changes) then the Current "for actual updates since the baseline) - then finally and not lease the Report file that contains all the changes.
{