From 15538e8559acaa2aa8fc414b9dac2c88de2833df Mon Sep 17 00:00:00 2001 From: Przemyslaw Klys Date: Wed, 23 Dec 2020 09:32:07 +0100 Subject: [PATCH] Improve Get-GPOZaurrBroken --- Private/Test-SysvolFolders.ps1 | 92 +++++++++++++--------------------- Public/Get-GPOZaurrBroken.ps1 | 74 +++++++++++++++++++++------ 2 files changed, 93 insertions(+), 73 deletions(-) diff --git a/Private/Test-SysvolFolders.ps1 b/Private/Test-SysvolFolders.ps1 index b87414c..1184dc2 100644 --- a/Private/Test-SysvolFolders.ps1 +++ b/Private/Test-SysvolFolders.ps1 @@ -9,8 +9,7 @@ ) $Differences = @{ } $SysvolHash = @{ } - - $GPOGUIDS = $GPOs.ID.GUID + $GPOGUIDS = ConvertFrom-DistinguishedName -DistinguishedName $GPOs.DistinguishedName $SysVolPath = "\\$($Server)\SYSVOL\$Domain\Policies" Write-Verbose "Get-GPOZaurrBroken - Processing SYSVOL from \\$($Server)\SYSVOL\$Domain\Policies" try { @@ -19,10 +18,10 @@ $Sysvol = $Null } foreach ($_ in $SYSVOL) { - $GUID = $_.Name -replace '{' -replace '}' + $GUID = $_.Name $SysvolHash[$GUID] = $_ } - $Files = $SYSVOL.Name -replace '{' -replace '}' + $Files = $SYSVOL.Name if ($Files) { $Comparing = Compare-Object -ReferenceObject $GPOGUIDS -DifferenceObject $Files -IncludeEqual foreach ($_ in $Comparing) { @@ -30,8 +29,10 @@ # we skip policy definitions continue } + $ADStatus = $PoliciesAD[$_.InputObject] if ($_.SideIndicator -eq '==') { - $Found = 'Exists' + #$Found = 'Exists' + $Found = $ADStatus } elseif ($_.SideIndicator -eq '<=') { $Found = 'Not available on SYSVOL' } elseif ($_.SideIndicator -eq '=>') { @@ -41,8 +42,10 @@ $Found = 'Not available in AD' } } else { + # This shouldn't happen at all $Found = 'Orphaned GPO' } + $Differences[$_.InputObject] = $Found } } @@ -50,85 +53,60 @@ $Count = 0 foreach ($GPO in $GPOS) { $Count++ - Write-Verbose "Get-GPOZaurrBroken - Processing [$($GPO.DomainName)]($Count/$($GPOS.Count)) $($GPO.DisplayName)" - if ($null -ne $SysvolHash[$GPO.Id.GUID].FullName) { - $FullPath = $SysvolHash[$GPO.Id.GUID].FullName - try { - $ACL = Get-Acl -Path $SysvolHash[$GPO.Id.GUID].FullName -ErrorAction Stop -Verbose:$false - $Owner = $ACL.Owner - $ErrorMessage = '' - } catch { - Write-Warning "Get-GPOZaurrBroken - ACL reading (1) failed for $FullPath with error: $($_.Exception.Message)" - $ACL = $null - $Owner = '' - $ErrorMessage = $_.Exception.Message - } + $GPOGuid = ConvertFrom-DistinguishedName -DistinguishedName $GPO.DistinguishedName + if ($GPO.DisplayName) { + $GPODisplayName = $GPO.DisplayName + $GPOName = $GPO.Name + Write-Verbose "Get-GPOZaurrBroken - Processing [$($Domain)]($Count/$($GPOS.Count)) $($GPO.DisplayName)" } else { - $FullPath = -join ($SysVolPath, "\{$($GPO.Id.Guid)}") - $ACL = $null - $Owner = '' + $GPOName = $GPOGuid + $GPODisplayName = $GPOGuid + Write-Verbose "Get-GPOZaurrBroken - Processing [$($Domain)]($Count/$($GPOS.Count)) $($GPOGuid)" + } + if ($null -ne $SysvolHash[$GPOGuid].FullName) { + $FullPath = $SysvolHash[$GPOGuid].FullName + $ErrorMessage = '' + } else { + $FullPath = -join ($SysVolPath, "\$($GPOGuid)") $ErrorMessage = 'Not found on SYSVOL' } - if ($null -eq $Differences[$GPO.Id.Guid]) { - $SysVolStatus = 'Unknown Issue' + if ($null -eq $Differences[$GPOGuid]) { + $SysVolStatus = 'Unknown issue' } else { - $SysVolStatus = $Differences[$GPO.Id.Guid] + $SysVolStatus = $Differences[$GPOGuid] } [PSCustomObject] @{ - DisplayName = $GPO.DisplayName + DisplayName = $GPODisplayName Status = $SysVolStatus - DomainName = $GPO.DomainName + DomainName = $Domain SysvolServer = $Server - SysvolStatus = $SysVolStatus - GpoStatus = $GPO.GpoStatus - Owner = $GPO.Owner - FileOwner = $Owner - Id = $GPO.Id.Guid + ObjectClass = $GPO.ObjectClass + Id = $GPOName Path = $FullPath - DistinguishedName = -join ("CN={", $GPO.Id.Guid, "},", $PoliciesSearchBase) + DistinguishedName = -join ("CN=", $GPOGuid, ",", $PoliciesSearchBase) Description = $GPO.Description - CreationTime = $GPO.CreationTime - ModificationTime = $GPO.ModificationTime - UserVersion = $GPO.UserVersion - ComputerVersion = $GPO.ComputerVersion - WmiFilter = $GPO.WmiFilter + CreationTime = $GPO.Created + ModificationTime = $GPO.Modified Error = $ErrorMessage } } # Now we need to list thru Sysvol files and fine those that do not exists as GPO and create dummy GPO objects to show orphaned gpos Write-Verbose "Get-GPOZaurrBroken - Processing SYSVOL differences" foreach ($_ in $Differences.Keys) { - if ($Differences[$_] -in 'Not available in AD', 'Permissions issue') { + if ($Differences[$_] -in 'Not available in AD') { $FullPath = $SysvolHash[$_].FullName - try { - $ACL = Get-Acl -Path $FullPath -ErrorAction Stop - $Owner = $ACL.Owner - $ErrorMessage = '' - } catch { - Write-Warning "Get-GPOZaurrBroken - ACL reading (2) failed for $FullPath with error: $($_.Exception.Message)" - $ACL = $null - $Owner = $null - $ErrorMessage = $_.Exception.Message - } - [PSCustomObject] @{ DisplayName = $SysvolHash[$_].BaseName Status = $Differences[$_] DomainName = $Domain SysvolServer = $Server - SysvolStatus = 'Exists' #$Differences[$GPO.Id.Guid] - GpoStatus = $Differences[$_] - Owner = '' - FileOwner = $Owner + ObjectClass = '' Id = $_ Path = $FullPath - DistinguishedName = -join ("CN={", $_, "},", $PoliciesSearchBase) + DistinguishedName = -join ("CN=", $_, ",", $PoliciesSearchBase) Description = $null CreationTime = $SysvolHash[$_].CreationTime ModificationTime = $SysvolHash[$_].LastWriteTime - UserVersion = $null - ComputerVersion = $null - WmiFilter = $null Error = $ErrorMessage } } diff --git a/Public/Get-GPOZaurrBroken.ps1 b/Public/Get-GPOZaurrBroken.ps1 index 35ab948..d984eac 100644 --- a/Public/Get-GPOZaurrBroken.ps1 +++ b/Public/Get-GPOZaurrBroken.ps1 @@ -1,4 +1,47 @@ function Get-GPOZaurrBroken { + <# + .SYNOPSIS + Detects broken or otherwise damaged Group Policies + + .DESCRIPTION + Detects broken or otherwise damaged Group Policies providing insight whether GPO exists in both AD and SYSVOL. + It provides few statuses: + - Permissions issue - means account couldn't read GPO due to permissions + - ObjectClass issue - means that ObjectClass is of type Container, rather than expected groupPolicyContainer + - Not available on SYSVOL - means SYSVOL data is missing, yet AD metadata is available + - Not available in AD - means AD metadata is missing, yet SYSVOL data is available + - Exists - means AD metadata and SYSVOL data are available + + .PARAMETER Forest + Target different Forest, by default current forest is used + + .PARAMETER ExcludeDomains + Exclude domain from search, by default whole forest is scanned + + .PARAMETER ExcludeDomainControllers + Exclude specific domain controllers, by default there are no exclusions, as long as VerifyDomainControllers switch is enabled. Otherwise this parameter is ignored. + + .PARAMETER IncludeDomains + Include only specific domains, by default whole forest is scanned + + .PARAMETER IncludeDomainControllers + Include only specific domain controllers, by default all domain controllers are included, as long as VerifyDomainControllers switch is enabled. Otherwise this parameter is ignored. + + .PARAMETER SkipRODC + Skip Read-Only Domain Controllers. By default all domain controllers are included. + + .PARAMETER ExtendedForestInformation + Ability to provide Forest Information from another command to speed up processing + + .PARAMETER VerifyDomainControllers + Forces cmdlet to check GPO Existance on Domain Controllers rather then per domain + + .EXAMPLE + Get-GPOZaurrBroken -Verbose | Format-Table + + .NOTES + General notes + #> [alias('Get-GPOZaurrSysvol')] [cmdletBinding()] param( @@ -8,7 +51,6 @@ [alias('Domain', 'Domains')][string[]] $IncludeDomains, [alias('DomainControllers')][string[]] $IncludeDomainControllers, [switch] $SkipRODC, - [Array] $GPOs, [System.Collections.IDictionary] $ExtendedForestInformation, [switch] $VerifyDomainControllers ) @@ -21,31 +63,31 @@ $PoliciesAD = @{} if ($SystemsContainer) { $PoliciesSearchBase = -join ("CN=Policies,", $SystemsContainer) - $PoliciesInAD = Get-ADObject -SearchBase $PoliciesSearchBase -SearchScope OneLevel -Filter * -Server $QueryServer + $PoliciesInAD = Get-ADObject -SearchBase $PoliciesSearchBase -SearchScope OneLevel -Filter * -Server $QueryServer -Properties Name, gPCFileSysPath, DisplayName, DistinguishedName, Description, Created, Modified, ObjectClass, ObjectGUID foreach ($Policy in $PoliciesInAD) { $GUIDFromDN = ConvertFrom-DistinguishedName -DistinguishedName $Policy.DistinguishedName - $GUIDFromDN = $GUIDFromDN -replace '{' -replace '}' - $GUID = $Policy.Name -replace '{' -replace '}' - if ($GUID -and $GUIDFromDN) { - $PoliciesAD[$GUIDFromDN] = 'Exists' + if ($Policy.ObjectClass -eq 'Container') { + # This usually means GPO deletion process somehow failed and while object itself stayed it isn't groupPolicyContainer anymore + $PoliciesAD[$GUIDFromDN] = 'ObjectClass issue' } else { - $PoliciesAD[$GUIDFromDN] = 'Permissions issue' + $GUID = $Policy.Name + if ($GUID -and $GUIDFromDN) { + $PoliciesAD[$GUIDFromDN] = 'Exists' + } else { + $PoliciesAD[$GUIDFromDN] = 'Permissions issue' + } } } + } else { + Write-Warning "Get-GPOZaurrBroken - Couldn't get GPOs from $Domain. Skipping" } - Try { - [Array]$GPOs = Get-GPO -All -Domain $Domain -Server $QueryServer - } catch { - Write-Warning "Get-GPOZaurrBroken - Couldn't get GPOs from $Domain. Error: $($_.Exception.Message)" - continue - } - if ($GPOs.Count -ge 2) { + if ($PoliciesInAD.Count -ge 2) { if (-not $VerifyDomainControllers) { - Test-SysVolFolders -GPOs $GPOs -Server $Domain -Domain $Domain -PoliciesAD $PoliciesAD -PoliciesSearchBase $PoliciesSearchBase + Test-SysVolFolders -GPOs $PoliciesInAD -Server $Domain -Domain $Domain -PoliciesAD $PoliciesAD -PoliciesSearchBase $PoliciesSearchBase } else { foreach ($Server in $ForestInformation['DomainDomainControllers']["$Domain"]) { Write-Verbose "Get-GPOZaurrBroken - Processing $Domain \ $($Server.HostName.Trim())" - Test-SysVolFolders -GPOs $GPOs -Server $Server.Hostname -Domain $Domain -PoliciesAD $PoliciesAD -PoliciesSearchBase $PoliciesSearchBase + Test-SysVolFolders -GPOs $PoliciesInAD -Server $Server.Hostname -Domain $Domain -PoliciesAD $PoliciesAD -PoliciesSearchBase $PoliciesSearchBase } } } else {