Compare commits

...

12 Commits

Author SHA1 Message Date
Przemyslaw Klys 7adfd1ef65 PSD1 2020-12-23 11:06:39 +01:00
Przemyslaw Klys 828c221ac9 Renamed GPOBroken 2020-12-23 10:46:07 +01:00
Przemyslaw Klys 8e80fd423d Update readme 2020-12-23 10:45:55 +01:00
Przemyslaw Klys f52fd60fef Update 2020-12-23 10:33:30 +01:00
Przemyslaw Klys 9594afb246 Update example 2020-12-23 10:00:53 +01:00
Przemyslaw Klys 7c52284b48 Improved Remove-GPOZaurrBroken 2020-12-23 09:47:14 +01:00
Przemyslaw Klys 2fd64952af Update readme 2020-12-23 09:47:04 +01:00
Przemyslaw Klys 15538e8559 Improve Get-GPOZaurrBroken 2020-12-23 09:32:07 +01:00
Przemyslaw Klys a36a470538 Update readme 2020-12-23 09:31:57 +01:00
Przemyslaw Klys 0a5d65761c PSD1 / README update 2020-12-21 13:22:20 +01:00
Przemyslaw Klys 7ed3cb6dad Update 2020-12-21 13:21:29 +01:00
Przemyslaw Klys 4d9edde212 Update example 2020-12-15 19:11:24 +01:00
11 changed files with 343 additions and 132 deletions
@@ -1,10 +1,10 @@
Import-Module "$PSScriptRoot\..\GPoZaurr.psd1" -Force
# Find broken GPOs
Get-GPOZaurrBroken -Verbose -IncludeDomains 'ad.evotec.pl' | Format-Table
Get-GPOZaurrBroken -Verbose | Format-Table # -IncludeDomains 'ad.evotec.pl' | Format-Table
# this allows you to process X amount of orphaned folders/files (good for testing)
Remove-GPOZaurrBroken -Verbose -WhatIf #-IncludeDomains 'ad.evotec.pl' #-LimitProcessing 2
Remove-GPOZaurrBroken -Verbose -WhatIf -Type AD, SYSVOL -LimitProcessing 1 #-IncludeDomains 'ad.evotec.pl' #-LimitProcessing 2
# this runs for whole SYSVOL and checks things against GPOS
Remove-GPOZaurrBroken -Verbose -IncludeDomains 'ad.evotec.xyz' -BackupPath $Env:UserProfile\Desktop\MyBackup1 -WhatIf
Remove-GPOZaurrBroken -Verbose -IncludeDomains 'ad.evotec.xyz' -BackupPath $Env:UserProfile\Desktop\MyBackup1 -WhatIf -Type AD, SYSVOL
+1 -1
View File
@@ -1,4 +1,4 @@
Import-Module "$PSScriptRoot\..\GPoZaurr.psd1" -Force
#$Output = Invoke-GPOZaurr -FilePath $PSScriptRoot\Reports\GPOZaurr.html -PassThru -Type GPOConsistency, GPOList, GPODuplicates, GPOOrphans, GPOOwners, NetLogonOwners, GPOPermissionsRead, GPOPermissionsAdministrative,GPOPermissionsUnknown
Invoke-GPOZaurr -FilePath $PSScriptRoot\Reports\GPOZaurr.html -Type GPOPermissions
Invoke-GPOZaurr -FilePath $PSScriptRoot\Reports\GPOZaurr.html -Type GPOPermissions,GPOPermissionsRead
+2 -2
View File
@@ -8,7 +8,7 @@
Description = 'Group Policy Eater is a PowerShell module that aims to gather information about Group Policies but also allows fixing issues that you may find in them.'
FunctionsToExport = @('Add-GPOPermission', 'Add-GPOZaurrPermission', 'Backup-GPOZaurr', 'Clear-GPOZaurrSysvolDFSR', 'ConvertFrom-CSExtension', 'Find-CSExtension', 'Get-GPOZaurr', 'Get-GPOZaurrAD', 'Get-GPOZaurrBackupInformation', 'Get-GPOZaurrBroken', 'Get-GPOZaurrDictionary', 'Get-GPOZaurrDuplicateObject', 'Get-GPOZaurrFiles', 'Get-GPOZaurrFilesPolicyDefinition', 'Get-GPOZaurrFolders', 'Get-GPOZaurrInheritance', 'Get-GPOZaurrLegacyFiles', 'Get-GPOZaurrLink', 'Get-GPOZaurrLinkSummary', 'Get-GPOZaurrNetLogon', 'Get-GPOZaurrOwner', 'Get-GPOZaurrPassword', 'Get-GPOZaurrPermission', 'Get-GPOZaurrPermissionAnalysis', 'Get-GPOZaurrPermissionConsistency', 'Get-GPOZaurrPermissionIssue', 'Get-GPOZaurrPermissionRoot', 'Get-GPOZaurrPermissionSummary', 'Get-GPOZaurrSysvolDFSR', 'Get-GPOZaurrWMI', 'Invoke-GPOZaurr', 'Invoke-GPOZaurrContent', 'Invoke-GPOZaurrPermission', 'Invoke-GPOZaurrSupport', 'New-GPOZaurrWMI', 'Optimize-GPOZaurr', 'Remove-GPOPermission', 'Remove-GPOZaurr', 'Remove-GPOZaurrBroken', 'Remove-GPOZaurrDuplicateObject', 'Remove-GPOZaurrFolders', 'Remove-GPOZaurrLegacyFiles', 'Remove-GPOZaurrPermission', 'Remove-GPOZaurrWMI', 'Repair-GPOZaurrNetLogonOwner', 'Repair-GPOZaurrPermission', 'Repair-GPOZaurrPermissionConsistency', 'Restore-GPOZaurr', 'Save-GPOZaurrFiles', 'Set-GPOOwner', 'Set-GPOZaurrOwner', 'Set-GPOZaurrStatus', 'Skip-GroupPolicy')
GUID = 'f7d4c9e4-0298-4f51-ad77-e8e3febebbde'
ModuleVersion = '0.0.99'
ModuleVersion = '0.0.101'
PowerShellVersion = '5.1'
PrivateData = @{
PSData = @{
@@ -18,7 +18,7 @@
}
}
RequiredModules = @(@{
ModuleVersion = '0.0.190'
ModuleVersion = '0.0.192'
ModuleName = 'PSSharedGoods'
Guid = 'ee272aa8-baaa-4edf-9f45-b6d6f7d844fe'
}, @{
+92 -41
View File
@@ -1,5 +1,5 @@
$GPOZaurrOrphans = [ordered] @{
Name = 'Orphaned Group Policies'
Name = 'Broken Group Policies'
Enabled = $true
ActionRequired = $null
Data = $null
@@ -7,41 +7,52 @@
Get-GPOZaurrBroken -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains
}
Processing = {
$Script:Reporting['GPOOrphans']['Variables']['ToBeDeletedPerDomain'] = @{}
$Script:Reporting['GPOOrphans']['Variables']['NotAvailablePermissionIssuePerDomain'] = @{}
foreach ($GPO in $Script:Reporting['GPOOrphans']['Data']) {
if (-not $Script:Reporting['GPOOrphans']['Variables']['ToBeDeletedPerDomain'][$GPO.DomainName]) {
$Script:Reporting['GPOOrphans']['Variables']['ToBeDeletedPerDomain'][$GPO.DomainName] = 0
$Script:Reporting['GPOBroken']['Variables']['ToBeDeletedPerDomain'] = @{}
$Script:Reporting['GPOBroken']['Variables']['NotAvailablePermissionIssuePerDomain'] = @{}
$Script:Reporting['GPOBroken']['Variables']['NotAvailableObjectClassIssuePerDomain'] = @{}
foreach ($GPO in $Script:Reporting['GPOBroken']['Data']) {
if (-not $Script:Reporting['GPOBroken']['Variables']['ToBeDeletedPerDomain'][$GPO.DomainName]) {
$Script:Reporting['GPOBroken']['Variables']['ToBeDeletedPerDomain'][$GPO.DomainName] = 0
}
if (-not $Script:Reporting['GPOOrphans']['Variables']['NotAvailablePermissionIssuePerDomain'][$GPO.DomainName]) {
$Script:Reporting['GPOOrphans']['Variables']['NotAvailablePermissionIssuePerDomain'][$GPO.DomainName] = 0
if (-not $Script:Reporting['GPOBroken']['Variables']['NotAvailablePermissionIssuePerDomain'][$GPO.DomainName]) {
$Script:Reporting['GPOBroken']['Variables']['NotAvailablePermissionIssuePerDomain'][$GPO.DomainName] = 0
}
if (-not $Script:Reporting['GPOBroken']['Variables']['NotAvailableObjectClassIssuePerDomain'][$GPO.DomainName]) {
$Script:Reporting['GPOBroken']['Variables']['NotAvailableObjectClassIssuePerDomain'][$GPO.DomainName] = 0
}
if ($GPO.Status -eq 'Not available in AD') {
$Script:Reporting['GPOOrphans']['Variables']['NotAvailableInAD']++
$Script:Reporting['GPOOrphans']['Variables']['ToBeDeleted']++
$Script:Reporting['GPOOrphans']['Variables']['ToBeDeletedPerDomain'][$GPO.DomainName]++
$Script:Reporting['GPOBroken']['Variables']['NotAvailableInAD']++
$Script:Reporting['GPOBroken']['Variables']['ToBeDeleted']++
$Script:Reporting['GPOBroken']['Variables']['ToBeDeletedPerDomain'][$GPO.DomainName]++
} elseif ($GPO.Status -eq 'Not available on SYSVOL') {
$Script:Reporting['GPOOrphans']['Variables']['NotAvailableOnSysvol']++
$Script:Reporting['GPOOrphans']['Variables']['ToBeDeleted']++
$Script:Reporting['GPOOrphans']['Variables']['ToBeDeletedPerDomain'][$GPO.DomainName]++
$Script:Reporting['GPOBroken']['Variables']['NotAvailableOnSysvol']++
$Script:Reporting['GPOBroken']['Variables']['ToBeDeleted']++
$Script:Reporting['GPOBroken']['Variables']['ToBeDeletedPerDomain'][$GPO.DomainName]++
} elseif ($GPO.Status -eq 'Permissions issue') {
$Script:Reporting['GPOOrphans']['Variables']['NotAvailablePermissionIssue']++
$Script:Reporting['GPOOrphans']['Variables']['NotAvailablePermissionIssuePerDomain'][$GPO.DomainName]++
$Script:Reporting['GPOBroken']['Variables']['NotAvailablePermissionIssue']++
$Script:Reporting['GPOBroken']['Variables']['NotAvailablePermissionIssuePerDomain'][$GPO.DomainName]++
} elseif ($GPO.Status -eq 'ObjectClass issue') {
$Script:Reporting['GPOBroken']['Variables']['NotAvailableObjectClassIssue']++
$Script:Reporting['GPOBroken']['Variables']['ToBeDeleted']++
$Script:Reporting['GPOBroken']['Variables']['NotAvailableObjectClassIssuePerDomain'][$GPO.DomainName]++
$Script:Reporting['GPOBroken']['Variables']['ToBeDeletedPerDomain'][$GPO.DomainName]++
}
}
if ($Script:Reporting['GPOOrphans']['Variables']['ToBeDeleted'] -gt 0) {
$Script:Reporting['GPOOrphans']['ActionRequired'] = $true
if ($Script:Reporting['GPOBroken']['Variables']['ToBeDeleted'] -gt 0) {
$Script:Reporting['GPOBroken']['ActionRequired'] = $true
} else {
$Script:Reporting['GPOOrphans']['ActionRequired'] = $false
$Script:Reporting['GPOBroken']['ActionRequired'] = $false
}
}
Variables = @{
NotAvailableInAD = 0
NotAvailableOnSysvol = 0
NotAvailablePermissionIssue = 0
NotAvailablePermissionIssuePerDomain = $null
ToBeDeleted = 0
ToBeDeletedPerDomain = $null
NotAvailableInAD = 0
NotAvailableOnSysvol = 0
NotAvailablePermissionIssue = 0
NotAvailablePermissionIssuePerDomain = $null
ToBeDeleted = 0
ToBeDeletedPerDomain = $null
NotAvailableObjectClassIssue = 0
NotAvailableObjectClassIssuePerDomain = $null
}
Overview = {
<#
@@ -58,15 +69,15 @@
}
New-HTMLText -Text 'Following chart presents ', 'Broken / Orphaned Group Policies' -FontSize 10pt -FontWeight normal, bold
New-HTMLList -Type Unordered {
New-HTMLListItem -Text 'Group Policies on SYSVOL, but no details in AD: ', $Script:Reporting['GPOOrphans']['Variables']['NotAvailableInAD'] -FontWeight normal, bold
New-HTMLListItem -Text 'Group Policies in AD, but no content on SYSVOL: ', $Script:Reporting['GPOOrphans']['Variables']['NotAvailableOnSysvol'] -FontWeight normal, bold
New-HTMLListItem -Text "Group Policies which couldn't be assed due to permissions issue: ", $Script:Reporting['GPOOrphans']['Variables']['NotAvailablePermissionIssue'] -FontWeight normal, bold
New-HTMLListItem -Text 'Group Policies on SYSVOL, but no details in AD: ', $Script:Reporting['GPOBroken']['Variables']['NotAvailableInAD'] -FontWeight normal, bold
New-HTMLListItem -Text 'Group Policies in AD, but no content on SYSVOL: ', $Script:Reporting['GPOBroken']['Variables']['NotAvailableOnSysvol'] -FontWeight normal, bold
New-HTMLListItem -Text "Group Policies which couldn't be assed due to permissions issue: ", $Script:Reporting['GPOBroken']['Variables']['NotAvailablePermissionIssue'] -FontWeight normal, bold
} -FontSize 10pt
New-HTMLText -FontSize 10pt -Text 'Those problems must be resolved before doing other clenaup activities.'
New-HTMLChart {
New-ChartBarOptions -Type barStacked
New-ChartLegend -Name 'Not in AD', 'Not on SYSVOL', 'Permissions Issue' -Color Crimson, LightCoral, IndianRed
New-ChartBar -Name 'Orphans' -Value $Script:Reporting['GPOOrphans']['Variables']['NotAvailableInAD'], $Script:Reporting['GPOOrphans']['Variables']['NotAvailableOnSysvol'], $Script:Reporting['GPOOrphans']['Variables']['NotAvailablePermissionIssue']
New-ChartBar -Name 'Orphans' -Value $Script:Reporting['GPOBroken']['Variables']['NotAvailableInAD'], $Script:Reporting['GPOBroken']['Variables']['NotAvailableOnSysvol'], $Script:Reporting['GPOBroken']['Variables']['NotAvailablePermissionIssue']
} -Title 'Broken / Orphaned Group Policies' -TitleAlignment center
}
#>
@@ -85,14 +96,15 @@
} -FontSize 10pt
New-HTMLText -Text 'Following problems were detected:' -FontSize 10pt -FontWeight bold
New-HTMLList -Type Unordered {
New-HTMLListItem -Text 'Group Policies on SYSVOL, but no details in AD: ', $Script:Reporting['GPOOrphans']['Variables']['NotAvailableInAD'] -FontWeight normal, bold
New-HTMLListItem -Text 'Group Policies in AD, but no content on SYSVOL: ', $Script:Reporting['GPOOrphans']['Variables']['NotAvailableOnSysvol'] -FontWeight normal, bold
New-HTMLListItem -Text "Group Policies which couldn't be assed due to permissions issue: ", $Script:Reporting['GPOOrphans']['Variables']['NotAvailablePermissionIssue'] -FontWeight normal, bold
New-HTMLListItem -Text 'Group Policies on SYSVOL, but no details in AD: ', $Script:Reporting['GPOBroken']['Variables']['NotAvailableInAD'] -FontWeight normal, bold
New-HTMLListItem -Text 'Group Policies in AD, but no content on SYSVOL: ', $Script:Reporting['GPOBroken']['Variables']['NotAvailableOnSysvol'] -FontWeight normal, bold
New-HTMLListItem -Text 'Group Policies which exists, but have wrong ObjectClass: ', $Script:Reporting['GPOBroken']['Variables']['NotAvailableObjectClassIssue'] -FontWeight normal, bold
New-HTMLListItem -Text "Group Policies which couldn't be assed due to permissions issue: ", $Script:Reporting['GPOBroken']['Variables']['NotAvailablePermissionIssue'] -FontWeight normal, bold
} -FontSize 10pt
New-HTMLText -Text 'Following domains require actions (permissions required):' -FontSize 10pt -FontWeight bold
New-HTMLList -Type Unordered {
foreach ($Domain in $Script:Reporting['GPOOrphans']['Variables']['ToBeDeletedPerDomain'].Keys) {
New-HTMLListItem -Text "$Domain requires ", $Script:Reporting['GPOOrphans']['Variables']['ToBeDeletedPerDomain'][$Domain], " changes." -FontWeight normal, bold, normal
foreach ($Domain in $Script:Reporting['GPOBroken']['Variables']['ToBeDeletedPerDomain'].Keys) {
New-HTMLListItem -Text "$Domain requires ", $Script:Reporting['GPOBroken']['Variables']['ToBeDeletedPerDomain'][$Domain], " changes." -FontWeight normal, bold, normal
}
} -FontSize 10pt
New-HTMLText -Text "Please review output in table and follow the steps below table to get Active Directory Group Policies in healthy state." -FontSize 10pt
@@ -100,25 +112,26 @@
Solution = {
New-HTMLSection -Invisible {
New-HTMLPanel {
& $Script:GPOConfiguration['GPOOrphans']['Summary']
& $Script:GPOConfiguration['GPOBroken']['Summary']
}
New-HTMLPanel {
New-HTMLChart {
New-ChartBarOptions -Type barStacked
New-ChartLegend -Name 'Not in AD', 'Not on SYSVOL', 'Permissions Issue' -Color Crimson, LightCoral, IndianRed
New-ChartBar -Name 'Orphans' -Value $Script:Reporting['GPOOrphans']['Variables']['NotAvailableInAD'], $Script:Reporting['GPOOrphans']['Variables']['NotAvailableOnSysvol'], $Script:Reporting['GPOOrphans']['Variables']['NotAvailablePermissionIssue']
New-ChartLegend -Name 'Not in AD', 'Not on SYSVOL', 'ObjectClass Issue', 'Permissions Issue' -Color Crimson, LightCoral, MediumOrchid, IndianRed
New-ChartBar -Name 'Broken' -Value $Script:Reporting['GPOBroken']['Variables']['NotAvailableInAD'], $Script:Reporting['GPOBroken']['Variables']['NotAvailableOnSysvol'], $Script:Reporting['GPOBroken']['Variables']['NotAvailableObjectClassIssue'], $Script:Reporting['GPOBroken']['Variables']['NotAvailablePermissionIssue']
} -Title 'Broken / Orphaned Group Policies' -TitleAlignment center
}
}
New-HTMLSection -Name 'Health State of Group Policies' {
New-HTMLTable -DataTable $Script:Reporting['GPOOrphans']['Data'] -Filtering {
New-HTMLTable -DataTable $Script:Reporting['GPOBroken']['Data'] -Filtering {
New-HTMLTableCondition -Name 'Status' -Value "Not available in AD" -BackgroundColor Salmon -ComparisonType string
New-HTMLTableCondition -Name 'Status' -Value "Not available on SYSVOL" -BackgroundColor LightCoral -ComparisonType string
New-HTMLTableCondition -Name 'Status' -Value "ObjectClass issue" -BackgroundColor MediumOrchid -ComparisonType string
New-HTMLTableCondition -Name 'Status' -Value "Permissions issue" -BackgroundColor MediumVioletRed -ComparisonType string -Color White
} -PagingOptions 10, 20, 30, 40, 50
}
if ($Script:Reporting['Settings']['HideSteps'] -eq $false) {
New-HTMLSection -Name 'Steps to fix - Not available on SYSVOL / Active Directory' {
New-HTMLSection -Name 'Steps to fix - Not available on SYSVOL / Active Directory / ObjectClass issue' {
New-HTMLContainer {
New-HTMLSpanStyle -FontSize 10pt {
New-HTMLWizard {
@@ -243,6 +256,44 @@
"If there's nothing else to be deleted on AD side, we can skip to next step step. "
}
}
New-HTMLWizardStep -Name 'Fix GPOs of wrong ObjectClass' {
New-HTMLText -Text @(
"Following command when executed runs cleanup procedure that removes all GPOs which have ObjectClass of Container, rather than required groupPolicyContainer. "
"Make sure when running it for the first time to run it with ",
"WhatIf",
" parameter as shown below to prevent accidental removal."
'When run it will remove GPO metadata from AD, and any files/folders from SYSVOL.'
) -FontWeight normal, normal, bold, normal -Color Black, Black, Red, Black
New-HTMLCodeBlock -Code {
Remove-GPOZaurrBroken -Type ObjectClass -WhatIf -Verbose
}
New-HTMLText -TextBlock {
"Alternatively for multi-domain scenario, if you have limited Domain Admin credentials to a single domain please use following command: "
}
New-HTMLCodeBlock -Code {
Remove-GPOZaurrBroken -Type ObjectClass -WhatIf -IncludeDomains 'YourDomainYouHavePermissionsFor' -Verbose
}
New-HTMLText -TextBlock {
"After execution please make sure there are no errors, make sure to review provided output, and confirm that what is about to be deleted matches expected data. "
"Keep in mind that there is no backup for this as backup process doesn't see GPOs that are of wrong ObjectClass. "
"Once you delete it, it's gone. "
} -LineBreak
New-HTMLText -Text 'Once happy with results please follow with command (this will start deletion process): ' -LineBreak -FontWeight bold
New-HTMLCodeBlock -Code {
Remove-GPOZaurrBroken -Type ObjectClass -LimitProcessing 2 -Verbose
}
New-HTMLText -TextBlock {
"Alternatively for multi-domain scenario, if you have limited Domain Admin credentials to a single domain please use following command: "
}
New-HTMLCodeBlock -Code {
Remove-GPOZaurrBroken -Type ObjectClass -LimitProcessing 2 -IncludeDomains 'YourDomainYouHavePermissionsFor' -Verbose
}
New-HTMLText -TextBlock {
"This command when executed deletes only first X broken GPOs. Use LimitProcessing parameter to prevent mass delete and increase the counter when no errors occur. "
"Repeat step above as much as needed increasing LimitProcessing count till there's nothing left. In case of any issues please review and action accordingly. "
"If there's nothing else to be deleted, we can skip to next step step. "
}
}
New-HTMLWizardStep -Name 'Verification report' {
New-HTMLText -TextBlock {
"Once cleanup task was executed properly, we need to verify that report now shows no problems."
@@ -257,9 +308,9 @@
}
}
}
if ($Script:Reporting['GPOOrphans']['WarningsAndErrors']) {
if ($Script:Reporting['GPOBroken']['WarningsAndErrors']) {
New-HTMLSection -Name 'Warnings & Errors to Review' {
New-HTMLTable -DataTable $Script:Reporting['GPOOrphans']['WarningsAndErrors'] -Filtering {
New-HTMLTable -DataTable $Script:Reporting['GPOBroken']['WarningsAndErrors'] -Filtering {
New-HTMLTableCondition -Name 'Type' -Value 'Warning' -BackgroundColor SandyBrown -ComparisonType string -Row
New-HTMLTableCondition -Name 'Type' -Value 'Error' -BackgroundColor Salmon -ComparisonType string -Row
}
+69 -2
View File
@@ -9,6 +9,7 @@
}
$Object['PermissionsPerRow'] = $Object['Permissions'] | ForEach-Object { $_ }
$Object['PermissionsAnalysis'] = Get-GPOZaurrPermissionAnalysis -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -Permissions $Object.Permissions
$Object['PermissionsIssues'] = Get-GPOZaurrPermissionIssue -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains
$Object
}
Processing = {
@@ -28,6 +29,31 @@
$Script:Reporting['GPOPermissions']['Variables']['WillNotFixPerDomain'] = @{}
$Script:Reporting['GPOPermissions']['Variables']['WillFixPerDomain'] = @{}
$Script:Reporting['GPOPermissions']['Variables']['ReadPerDomain'] = @{}
$Script:Reporting['GPOPermissions']['Variables']['CouldNotReadPerDomain'] = @{}
$Script:Reporting['GPOPermissions']['Variables']['TotalPerDomain'] = @{}
foreach ($GPO in $Script:Reporting['GPOPermissions']['Data'].PermissionsIssues) {
# Create Per Domain Variables
if (-not $Script:Reporting['GPOPermissions']['Variables']['CouldNotReadPerDomain'][$GPO.DomainName]) {
$Script:Reporting['GPOPermissions']['Variables']['CouldNotReadPerDomain'][$GPO.DomainName] = 0
}
if (-not $Script:Reporting['GPOPermissions']['Variables']['ReadPerDomain'][$GPO.DomainName]) {
$Script:Reporting['GPOPermissions']['Variables']['ReadPerDomain'][$GPO.DomainName] = 0
}
if (-not $Script:Reporting['GPOPermissions']['Variables']['TotalPerDomain'][$GPO.DomainName]) {
$Script:Reporting['GPOPermissions']['Variables']['TotalPerDomain'][$GPO.DomainName] = 0
}
if ($GPO.PermissionIssue) {
$Script:Reporting['GPOPermissions']['Variables']['CouldNotRead']++
$Script:Reporting['GPOPermissions']['Variables']['CouldNotReadPerDomain'][$GPO.DomainName]++
} else {
$Script:Reporting['GPOPermissions']['Variables']['Read']++
$Script:Reporting['GPOPermissions']['Variables']['ReadPerDomain'][$GPO.DomainName]++
}
$Script:Reporting['GPOPermissions']['Variables']['Total']++
$Script:Reporting['GPOPermissions']['Variables']['TotalPerDomain'][$GPO.DomainName]++
}
foreach ($GPO in $Script:Reporting['GPOPermissions']['Data'].PermissionsAnalysis) {
# Create Per Domain Variables
if (-not $Script:Reporting['GPOPermissions']['Variables']['WillFixPerDomain'][$GPO.DomainName]) {
@@ -99,13 +125,21 @@
$Script:Reporting['GPOPermissions']['Variables']['WillNotFixPerDomain'][$GPO.DomainName]++
}
}
if ($Script:Reporting['GPOPermissions']['Variables']['WillFix'] -gt 0) {
if ($Script:Reporting['GPOPermissions']['Variables']['WillFix'] -gt 0 -or $Script:Reporting['GPOPermissions']['Variables']['CouldNotRead'] -gt 0) {
$Script:Reporting['GPOPermissions']['ActionRequired'] = $true
} else {
$Script:Reporting['GPOPermissions']['ActionRequired'] = $false
}
}
Variables = @{
# Issues / Couldnt read report
Read = 0
ReadPerDomain = $null
CouldNotRead = 0
CouldNotReadPerDomain = $null
TotalPerDomain = $null
Total = 0
# Permissions analysis
WillFix = 0
WillNotFix = 0
WillFixAdministrative = 0
@@ -150,7 +184,34 @@
New-HTMLListItem -Text "After MS16-072 is installed, user group policies are retrieved by using the computer's security context."
} -FontSize 10pt
New-HTMLText -FontSize 10pt -Text "Assesment results " -FontWeight bold
New-HTMLText -FontSize 10pt -Text @(
"Unfortunetly it's not as simple as it sounds. While checking for permissions mostly works fine, it's possible a Group Policy has totally removed account permissions from being able to asses any of it. ",
"The account we're using "
"$($Env:USERDOMAIN)\$($Env:USERNAME.ToUpper())",
" may simply not have enough permisions to properly asses permissions for a GPO. "
"Therefore we're using dual assesment for this situation, where first assesment is checking for GPO visibility or lack of it, and second assesment is checking for direct permissions assignement. "
"We just were able to detect the problem, but hopefully higher level account (Domain Admin) should be able to provide full assesment. "
) -FontWeight normal, normal, bold, normal -Color None, None, BlueDiamond, none, none
New-HTMLText -FontSize 10pt -Text "First assesment results: " -FontWeight bold
New-HTMLList -Type Unordered {
New-HTMLListItem -Text "Group Policies couldn't read at all: ", $Script:Reporting['GPOPermissions']['Variables']['CouldNotRead'] -FontWeight normal, bold
New-HTMLListItem -Text "Group Policies with permissions allowing read: ", $Script:Reporting['GPOPermissions']['Variables']['Read'] -FontWeight normal, bold
} -FontSize 10pt
New-HTMLText -Text 'Following domains require actions (permissions required):' -FontSize 10pt -FontWeight bold
New-HTMLList -Type Unordered {
foreach ($Domain in $Script:Reporting['GPOPermissions']['Variables']['CouldNotReadPerDomain'].Keys) {
New-HTMLListItem -Text @(
"$Domain requires ",
$Script:Reporting['GPOPermissions']['Variables']['CouldNotReadPerDomain'][$Domain],
" changes out of ",
$Script:Reporting['GPOPermissions']['Variables']['TotalPerDomain'][$Domain],
"."
) -FontWeight normal, bold, normal
}
} -FontSize 10pt
New-HTMLText -FontSize 10pt -Text "Second Assesment results " -FontWeight bold
New-HTMLList -Type Unordered {
New-HTMLListItem -Text 'Group Policies requiring Authenticated Users with GpoRead permission: ', $Script:Reporting['GPOPermissions']['Variables']['WillFixAuthenticatedUsers'] -FontWeight normal, bold
New-HTMLListItem -Text "Group Policies which don't require changes: ", $Script:Reporting['GPOPermissions']['Variables']['WillNotTouchAuthenticatedUsers'] -FontWeight normal, bold
@@ -229,6 +290,7 @@
New-HTMLChart {
New-ChartBarOptions -Type barStacked
New-ChartLegend -Name 'Yes', 'No' -Color SpringGreen, Salmon
New-ChartBar -Name 'Visible Permissions' -Value $Script:Reporting['GPOPermissions']['Variables']['Read'], $Script:Reporting['GPOPermissions']['Variables']['CouldNotRead']
New-ChartBar -Name 'Overall Permissions' -Value $Script:Reporting['GPOPermissions']['Variables']['WillNotFix'], $Script:Reporting['GPOPermissions']['Variables']['WillFix']
New-ChartBar -Name 'Administrative Permissions' -Value $Script:Reporting['GPOPermissions']['Variables']['WillNotTouchAdministrative'], $Script:Reporting['GPOPermissions']['Variables']['WillFixAdministrative']
New-ChartBar -Name 'Authenticated Users Permissions' -Value $Script:Reporting['GPOPermissions']['Variables']['WillNotTouchAdministrative'], $Script:Reporting['GPOPermissions']['Variables']['WillFixAuthenticatedUsers']
@@ -237,6 +299,11 @@
} -Title 'Group Policy Permissions' -TitleAlignment center
}
}
New-HTMLSection -Name 'Group Policy Visibility Analysis' {
New-HTMLTable -DataTable $Script:Reporting['GPOPermissions']['Data'].PermissionsIssues -Filtering {
New-HTMLTableCondition -Name 'PermissionIssue' -Value $true -BackgroundColor Salmon -ComparisonType string -Row
} -PagingOptions 7, 15, 30, 45, 60 -DefaultSortColumn PermissionIssue -DefaultSortOrder Descending
}
New-HTMLSection -Name 'Group Policy Permissions Analysis' {
New-HTMLContainer {
New-HTMLText -Text 'Explanation to table columns:' -FontSize 10pt
+2 -2
View File
@@ -6,7 +6,7 @@
Execute = {
[ordered] @{
Permissions = Get-GPOZaurrPermission -Type AuthenticatedUsers -ReturnSecurityWhenNoData -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains
Issues = Get-GPOZaurrPermissionIssue
Issues = Get-GPOZaurrPermissionIssue -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains
}
}
Processing = {
@@ -93,7 +93,7 @@
} -FontStyle italic -FontSize 10pt -FontWeight bold -LineBreak
New-HTMLText -FontSize 10pt -Text @(
"There are two parts to this assesment. Reading all Group Policies Permissions that account ",
$($Env:USERNAME.ToUpper()),
"$($Env:USERDOMAIN)\$($Env:USERNAME.ToUpper())",
" has permissions to read and provide detailed assesment about permissions. ",
"Second assesment checks for permissions that this account is not able to read at all, and therefore it has no visibility about permissions set on it. "
"We just were able to detect the problem, but hopefully higher level account (Domain Admin) should be able to provide full assesment. "
+1 -1
View File
@@ -1,5 +1,5 @@
$Script:GPOConfiguration = [ordered] @{
GPOOrphans = $GPOZaurrOrphans
GPOBroken = $GPOZaurrOrphans
GPOOwners = $GPOZaurrOwners
GPOConsistency = $GPOZaurrConsistency
GPODuplicates = $GPOZaurrDuplicates
+35 -57
View File
@@ -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
}
}
+58 -16
View File
@@ -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 {
+58 -6
View File
@@ -1,8 +1,54 @@
function Remove-GPOZaurrBroken {
<#
.SYNOPSIS
Finds and removes broken Group Policies from SYSVOL or AD or both.
.DESCRIPTION
Finds and removes broken Group Policies from SYSVOL or AD or both. Assesment is based on Get-GPOZaurrBroken and there are 3 supported types:
- AD - meaning GPOs which have no SYSVOL content will be deleted from AD
- SYSVOL - meaning GPOs which have no AD content will be deleted from SYSVOL
- ObjectClass - meaning GPOs which have ObjectClass category of Container rather than groupPolicyContainer will be deleted from AD & SYSVOL
.PARAMETER Type
Choose one or more types to delete. Options are AD, ObjectClass, SYSVOL
.PARAMETER BackupPath
Path to optional backup of SYSVOL content before deletion
.PARAMETER BackupDated
Forces backup to be created within folder that has date in it
.PARAMETER LimitProcessing
Allows to specify maximum number of items that will be fixed in a single run. It doesn't affect amount of GPOs processed
.PARAMETER Forest
Target different Forest, by default current forest is used
.PARAMETER ExcludeDomains
Exclude domain from search, by default whole forest is scanned
.PARAMETER IncludeDomains
Include only specific domains, by default whole forest is scanned
.PARAMETER ExtendedForestInformation
Ability to provide Forest Information from another command to speed up processing
.EXAMPLE
Remove-GPOZaurrBroken -Verbose -WhatIf -Type AD, SYSVOL
.EXAMPLE
Remove-GPOZaurrBroken -Verbose -WhatIf -Type AD, SYSVOL -IncludeDomains 'ad.evotec.pl' -LimitProcessing 2
.EXAMPLE
Remove-GPOZaurrBroken -Verbose -IncludeDomains 'ad.evotec.xyz' -BackupPath $Env:UserProfile\Desktop\MyBackup1 -WhatIf -Type AD, SYSVOL
.NOTES
General notes
#>
[alias('Remove-GPOZaurrOrphaned')]
[cmdletBinding(SupportsShouldProcess)]
param(
[ValidateSet('SYSVOL', 'AD')][string[]] $Type = @('SYSVOL', 'AD'),
[parameter(Mandatory, Position = 0)][ValidateSet('SYSVOL', 'AD', 'ObjectClass')][string[]] $Type,
[string] $BackupPath,
[switch] $BackupDated,
[int] $LimitProcessing = [int32]::MaxValue,
@@ -32,10 +78,15 @@
$_
}
}
if ($Type -contains 'ObjectClass') {
if ($_.Status -eq 'ObjectClass issue') {
$_
}
}
} | Select-Object | Select-Object -First $LimitProcessing | ForEach-Object {
$GPO = $_
if ($GPO.Status -eq 'Not available in AD') {
Write-Verbose "Remove-GPOZaurrBroken - Processing [AD] $($GPO.Path)"
if ($GPO.Status -in 'Not available in AD', 'ObjectClass issue') {
Write-Verbose "Remove-GPOZaurrBroken - Removing from SYSVOL [$($GPO.Status)] $($GPO.Path)"
if ($BackupFinalPath) {
Try {
Write-Verbose "Remove-GPOZaurrBroken - Backing up $($GPO.Path)"
@@ -47,15 +98,16 @@
}
}
if ($BackupWorked -or $BackupFinalPath -eq '') {
Write-Verbose "Remove-GPOZaurrBroken - Deleting $($GPO.Path)"
Write-Verbose "Remove-GPOZaurrBroken - Removing $($GPO.Path)"
try {
Remove-Item -Recurse -Force -LiteralPath $GPO.Path -ErrorAction Stop
} catch {
Write-Warning "Remove-GPOZaurrBroken - Failed to remove file $($GPO.Path): $($_.Exception.Message)."
}
}
} elseif ($GPO.Status -eq 'Not available on SYSVOL') {
Write-Verbose "Remove-GPOZaurrBroken - Processing [SYSVOL] $($GPO.DistinguishedName)"
}
if ($GPO.Status -in 'Not available on SYSVOL', 'ObjectClass issue') {
Write-Verbose "Remove-GPOZaurrBroken - Removing from AD [$($GPO.Status)] $($GPO.DistinguishedName)"
try {
$ExistingObject = Get-ADObject -Identity $GPO.DistinguishedName -Server $GPO.DomainName -ErrorAction Stop
} catch {
+22 -1
View File
@@ -57,7 +57,28 @@ That's it. Whenever there's a new version, you run the command, and you can enjo
## Changelog
- 0.0.99 - 13.10.2020
- 0.0.101 - 23.12.2020
- [x] Improved `Get-GPOZaurrBroken`
- [x] It now detects `ObjectClass Issue`
- [x] Heavily improved performance
- [x] Removed some useless properties for this particular cmdlet
- [x] All states: `Not available on SYSVOL`, `Not available in AD`, `Exists`, `Permissions Issue`, `ObjectClass Issue`
- [x] Improved help
- [x] Improved `Remove-GPOZaurrBroken`
- [x] It now deals with `ObjectClass Issue`
- [x] Heavily improved performance
- [x] Removed some useless properties for this particular cmdlet
- [x] Now requires manual type insert AD, SYSVOL or ObjectClass (or all of them). Before it was auto using AD/SYSVOL.
- [x] Improved help
- [x] Improved `Invoke-GPOZaurr`
- [x] Type `GPOList`
- [x] Renamed `GPOOrphans` to `GPOBroken`
- [x] Improved `GPOBroken` with `ObjectClass issue`
- 0.0.100 - 21.12.2020
- [x] Improved `Invoke-GPOZaurr`
- [x] Type `GPOPermissionsRead`
- [x] Type `GPOPermissions`
- 0.0.99 - 13.12.2020
- [x] Improved `Invoke-GPOZaurr`
- [x] Type `GPOList` - require GPO to be 7 days old for deletion to be proposed
- [x] Type `GPOPermissions` - one stop for permissions