diff --git a/Examples/Example-01-BackupGPOs.ps1 b/Examples/Example-01-BackupGPOs.ps1 new file mode 100644 index 0000000..e9d283d --- /dev/null +++ b/Examples/Example-01-BackupGPOs.ps1 @@ -0,0 +1,9 @@ +Import-Module "$PSScriptRoot\..\GPoZaurr.psd1" -Force + +# Backup GPOs +$BackupPath = "$Env:UserProfile\Desktop\GPO" +$GPOSummary = Backup-GPOZaurr -BackupPath $BackupPath -Verbose -Type EmptyAndUnlinked +$GPOSummary | Format-Table -AutoSize + +# Confirm GPOs are backed up properly +#Get-GPOZaurrBackupInformation -BackupFolder $GPOSummary[0].BackupDirectory | Format-Table -a \ No newline at end of file diff --git a/Examples/Example-02-RemoveEmptyAndUnlinkedGPOs.ps1 b/Examples/Example-02-RemoveEmptyAndUnlinkedGPOs.ps1 new file mode 100644 index 0000000..a710ebb --- /dev/null +++ b/Examples/Example-02-RemoveEmptyAndUnlinkedGPOs.ps1 @@ -0,0 +1,12 @@ +Import-Module "$PSScriptRoot\..\GPoZaurr.psd1" -Force + +# Backup GPOs +$BackupPath = "$Env:UserProfile\Desktop\GPO" +$GPOSummary = Backup-GPOZaurr -BackupPath $BackupPath -Verbose -Type EmptyAndUnlinked +$GPOSummary | Format-Table -AutoSize + +# Confirm GPOs are backed up properly +#Get-GPOZaurrBackupInformation -BackupFolder $GPOSummary[0].BackupDirectory | Format-Table -a + +# Remove GPOS +Remove-GPOZaurr -Type EmptyAndUnlinked -Verbose \ No newline at end of file diff --git a/Examples/Example-03-RestoreGPO.ps1 b/Examples/Example-03-RestoreGPO.ps1 new file mode 100644 index 0000000..ba730d7 --- /dev/null +++ b/Examples/Example-03-RestoreGPO.ps1 @@ -0,0 +1,14 @@ +Import-Module "$PSScriptRoot\..\GPoZaurr.psd1" -Force + +$RestoreFrom = 'C:\Users\przemyslaw.klys\Desktop\GPO\2020-04-02_20_20_04' + +$BackupInformation = Get-GPOZaurrBackupInformation -BackupFolder $RestoreFrom +$BackupInformation | Format-Table -a + +# restore all gpos +$RestoredGPOs = Restore-GPOZaurr -BackupFolder $RestoreFrom -Verbose +$RestoredGPOs | Format-Table -AutoSize + +# restore just one Gpo +$RestoredGPOs = Restore-GPOZaurr -BackupFolder $RestoreFrom -Verbose -DisplayName 'Users | Synced Office 365 Users' +$RestoredGPOs | Format-Table -AutoSize \ No newline at end of file diff --git a/Examples/Test.ps1 b/Examples/Test.ps1 new file mode 100644 index 0000000..468e3c4 --- /dev/null +++ b/Examples/Test.ps1 @@ -0,0 +1,5 @@ +Import-Module "$PSScriptRoot\..\GPoZaurr.psd1" -Force + +Get-GPOZaurr | ForEach { + Write-Warning "Write-Warning " +} \ No newline at end of file diff --git a/GPOZaurr.psd1 b/GPOZaurr.psd1 index 3aea644..06aa1ec 100644 --- a/GPOZaurr.psd1 +++ b/GPOZaurr.psd1 @@ -1,122 +1,25 @@ -# -# Module manifest for module 'GPOZaurr' -# -# Generated by: Przemyslaw Klys -# -# Generated on: 24.02.2020 -# - -@{ - - # Script module or binary module file associated with this manifest. - RootModule = 'GPOZaurr.psm1' - - # Version number of this module. - ModuleVersion = '0.0.4' - - # Supported PSEditions - CompatiblePSEditions = 'Desktop' - - # ID used to uniquely identify this module - GUID = 'f7d4c9e4-0298-4f51-ad77-e8e3febebbde' - - # Author of this module +@{ + AliasesToExport = '' Author = 'Przemyslaw Klys' - - # Company or vendor of this module CompanyName = 'Evotec' - - # Copyright statement for this module + CompatiblePSEditions = 'Desktop' Copyright = '(c) 2011 - 2020 Przemyslaw Klys @ Evotec. All rights reserved.' - - # Description of the functionality provided by this module Description = 'Group Policy Eater' - - # Minimum version of the Windows PowerShell engine required by this module + FunctionsToExport = 'Backup-GPOZaurr', 'Get-GPOZaurr', 'Get-GPOZaurrBackupInformation', 'Get-GPOZaurrPassword', 'Remove-GPOZaurr', 'Restore-GPOZaurr', 'Save-GPOZaurrFiles' + GUID = 'f7d4c9e4-0298-4f51-ad77-e8e3febebbde' + ModuleVersion = '0.0.4' PowerShellVersion = '5.1' - - # Name of the Windows PowerShell host required by this module - # PowerShellHostName = '' - - # Minimum version of the Windows PowerShell host required by this module - # PowerShellHostVersion = '' - - # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. - # DotNetFrameworkVersion = '' - - # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. - # CLRVersion = '' - - # Processor architecture (None, X86, Amd64) required by this module - # ProcessorArchitecture = '' - - # Modules that must be imported into the global environment prior to importing this module - RequiredModules = @(@{ModuleName = 'PSSharedGoods'; GUID = 'ee272aa8-baaa-4edf-9f45-b6d6f7d844fe'; ModuleVersion = '0.0.123'; }) - - # Assemblies that must be loaded prior to importing this module - # RequiredAssemblies = @() - - # Script files (.ps1) that are run in the caller's environment prior to importing this module. - # ScriptsToProcess = @() - - # Type files (.ps1xml) to be loaded when importing this module - # TypesToProcess = @() - - # Format files (.ps1xml) to be loaded when importing this module - # FormatsToProcess = @() - - # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess - # NestedModules = @() - - # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = 'Get-GPOZaurr', 'Get-GPOZaurrPassword', 'Save-GPOZaurrFiles' - - # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. - CmdletsToExport = @() - - # Variables to export from this module - # VariablesToExport = @() - - # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. - AliasesToExport = @() - - # DSC resources to export from this module - # DscResourcesToExport = @() - - # List of all modules packaged with this module - # ModuleList = @() - - # List of all files packaged with this module - # FileList = @() - - # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. PrivateData = @{ - PSData = @{ - - # Tags applied to this module. These help with module discovery in online galleries. - Tags = 'Windows', 'ActiveDirectory', 'GPO' - - # A URL to the license for this module. - # LicenseUri = '' - - # A URL to the main website for this project. - ProjectUri = 'https://github.com/EvotecIT/GPOZaurr' - - # A URL to an icon representing this module. - # IconUri = '' - - # ReleaseNotes of this module - # ReleaseNotes = '' - - } # End of PSData hashtable - - } # End of PrivateData hashtable - - # HelpInfo URI of this module - # HelpInfoURI = '' - - # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. - # DefaultCommandPrefix = '' - + Tags = 'Windows', 'ActiveDirectory', 'GPO' + ProjectUri = 'https://github.com/EvotecIT/GPOZaurr' + ExternalModuleDependencies = 'ActiveDirectory', 'GroupPolicy' + } + } + RequiredModules = @{ + ModuleVersion = '0.0.132' + ModuleName = 'PSSharedGoods' + Guid = 'ee272aa8-baaa-4edf-9f45-b6d6f7d844fe' + }, 'ActiveDirectory', 'GroupPolicy' + RootModule = 'GPOZaurr.psm1' } \ No newline at end of file diff --git a/Public/Backup-GPOZaurr.ps1 b/Public/Backup-GPOZaurr.ps1 new file mode 100644 index 0000000..39c3d4b --- /dev/null +++ b/Public/Backup-GPOZaurr.ps1 @@ -0,0 +1,81 @@ +function Backup-GPOZaurr { + [cmdletBinding()] + param( + [int] $LimitProcessing, + [validateset('All', 'Empty', 'Unlinked', 'EmptyAndUnlinked')][string] $Type = 'All', + [alias('ForestName')][string] $Forest, + [string[]] $ExcludeDomains, + [alias('Domain', 'Domains')][string[]] $IncludeDomains, + [System.Collections.IDictionary] $ExtendedForestInformation, + [string[]] $GPOPath, + [string] $BackupPath + ) + # Logging Paths + $DateDirectory = "$BackupPath\$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss'))" + Write-Verbose "Backup-GPOZaurr - Backing up to $DateDirectory" + + $GPOs = Get-GPOZaurr -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation -GPOPath $GPOPath + #$ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation + + $Count = 0 + $null = New-Item -ItemType Directory -Path $DateDirectory -Force + $GPOSummary = foreach ($GPO in $GPOs) { + #$QueryServer = $ForestInformation['QueryServers'][$GPO.Domain]['HostName'][0] + if ($Type -eq 'All') { + Write-Verbose "Backup-GPOZaurr - Backing up GPO $($GPO.Name) from $($GPO.Domain)" + $Count++ + try { + $BackupInfo = Backup-GPO -Name $GPO.Name -Domain $GPO.Domain -Path $DateDirectory #-Server $QueryServer + $BackupInfo + } catch { + Write-Warning "Backup-GPOZaurr - Backing up GPO $($GPO.Name) from $($GPO.Domain) using server $QueryServer failed: $($_.Exception.Message)" + } + if ($LimitProcessing -eq $Count) { + break + } + } elseif ($Type -eq 'Empty') { + if ($GPO.ComputerSettingsAvailable -eq $false -and $GPO.UserSettingsAvailable -eq $false) { + Write-Verbose "Backup-GPOZaurr - Backing up GPO $($GPO.Name) from $($GPO.Domain)" + $Count++ + try { + $BackupInfo = Backup-GPO -Name $GPO.Name -Domain $GPO.Domain -Path $DateDirectory #-Server $QueryServer + $BackupInfo + } catch { + Write-Warning "Backup-GPOZaurr - Backing up GPO $($GPO.Name) from $($GPO.Domain) using server $QueryServer failed: $($_.Exception.Message)" + } + if ($LimitProcessing -eq $Count) { + break + } + } + } elseif ($Type -eq 'EmptyAndUnlinked') { + if ($GPO.ComputerSettingsAvailable -eq $false -and $GPO.UserSettingsAvailable -eq $false -or $Gpo.Linked -eq $false) { + Write-Verbose "Backup-GPOZaurr - Backing up GPO $($GPO.Name) from $($GPO.Domain)" + $Count++ + try { + $BackupInfo = Backup-GPO -Name $GPO.Name -Domain $GPO.Domain -Path $DateDirectory #-Server $QueryServer + $BackupInfo + } catch { + Write-Warning "Backup-GPOZaurr - Backing up GPO $($GPO.Name) from $($GPO.Domain) using server $QueryServer failed: $($_.Exception.Message)" + } + if ($LimitProcessing -eq $Count) { + break + } + } + } elseif ($Type -eq 'Unlinked') { + if ($Gpo.Linked -eq $false) { + Write-Verbose "Backup-GPOZaurr - Backing up GPO $($GPO.Name) from $($GPO.Domain)" + $Count++ + try { + $BackupInfo = Backup-GPO -Name $GPO.Name -Domain $GPO.Domain -Path $DateDirectory #-Server $QueryServer + $BackupInfo + } catch { + Write-Warning "Backup-GPOZaurr - Backing up GPO $($GPO.Name) from $($GPO.Domain) using server $QueryServer failed: $($_.Exception.Message)" + } + if ($LimitProcessing -eq $Count) { + break + } + } + } + } + $GPOSummary +} \ No newline at end of file diff --git a/Public/Get-GPOZaurr.ps1 b/Public/Get-GPOZaurr.ps1 index 8d99e57..29e6269 100644 --- a/Public/Get-GPOZaurr.ps1 +++ b/Public/Get-GPOZaurr.ps1 @@ -7,26 +7,30 @@ [System.Collections.IDictionary] $ExtendedForestInformation, [string[]] $GPOPath ) - if (-not $GPOPath) { - if (-not $ExtendedForestInformation) { - $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains - } else { - $ForestInformation = $ExtendedForestInformation + Begin { + if (-not $GPOPath) { + $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation } - - foreach ($Domain in $ForestInformation.Domains) { - Get-GPO -All -Server $ForestInformation.QueryServers[$Domain].HostName[0] -Domain $Domain | ForEach-Object { - $XMLContent = Get-GPOReport -ID $_.ID -ReportType XML -Server $ForestInformation.QueryServers[$Domain].HostName[0] -Domain $Domain - Get-XMLGPO -XMLContent $XMLContent + } + Process { + if (-not $GPOPath) { + foreach ($Domain in $ForestInformation.Domains) { + Get-GPO -All -Server $ForestInformation.QueryServers[$Domain].HostName[0] -Domain $Domain | ForEach-Object { + $XMLContent = Get-GPOReport -ID $_.ID -ReportType XML -Server $ForestInformation.QueryServers[$Domain].HostName[0] -Domain $Domain + Get-XMLGPO -XMLContent $XMLContent + } } - } - } else { - foreach ($Path in $GPOPath) { - Get-ChildItem -LiteralPath $Path -Recurse -Filter *.xml | ForEach-Object { - $XMLContent = [XML]::new() - $XMLContent.Load($_.FullName) - Get-XMLGPO -XMLContent $XMLContent + } else { + foreach ($Path in $GPOPath) { + Get-ChildItem -LiteralPath $Path -Recurse -Filter *.xml | ForEach-Object { + $XMLContent = [XML]::new() + $XMLContent.Load($_.FullName) + Get-XMLGPO -XMLContent $XMLContent + } } } } -} + End { + + } +} \ No newline at end of file diff --git a/Public/Get-GPOZaurrBackupInformation.ps1 b/Public/Get-GPOZaurrBackupInformation.ps1 new file mode 100644 index 0000000..62f43f1 --- /dev/null +++ b/Public/Get-GPOZaurrBackupInformation.ps1 @@ -0,0 +1,26 @@ +function Get-GPOZaurrBackupInformation { + [cmdletBinding()] + param( + [string] $BackupFolder + ) + if ($BackupFolder) { + if ((Test-Path -LiteralPath "$BackupFolder\manifest.xml")) { + [xml] $Xml = Get-Content -LiteralPath "$BackupFolder\manifest.xml" + [Array] $BackupInformation = $Xml.Backups.BackupInst | ForEach-Object { + [PSCustomObject] @{ + DisplayName = $_.GPODisplayName.'#cdata-section' + Domain = $_.GPODomain.'#cdata-section' + Guid = $_.GPOGUid.'#cdata-section' + DomainGuid = $_.GPODomainGuid.'#cdata-section' + DomainController = $_.GPODomainController.'#cdata-section' + BackupTime = $_.BackupTime.'#cdata-section' + ID = $_.ID.'#cdata-section' + Comment = $_.Comment.'#cdata-section' + } + } + $BackupInformation + } else { + Write-Warning "Get-GPOZaurrBackupInformation - No backup information available" + } + } +} \ No newline at end of file diff --git a/Public/Remove-GPOZaurr.ps1 b/Public/Remove-GPOZaurr.ps1 new file mode 100644 index 0000000..d440d99 --- /dev/null +++ b/Public/Remove-GPOZaurr.ps1 @@ -0,0 +1,47 @@ +function Remove-GPOZaurr { + [cmdletBinding(SupportsShouldProcess)] + param( + [parameter(Mandatory)][validateset('Empty', 'Unlinked', 'EmptyAndUnlinked')][string] $Type, + [int] $LimitProcessing, + [alias('ForestName')][string] $Forest, + [string[]] $ExcludeDomains, + [alias('Domain', 'Domains')][string[]] $IncludeDomains, + [System.Collections.IDictionary] $ExtendedForestInformation, + [string[]] $GPOPath + ) + $GPOs = Get-GPOZaurr -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation -GPOPath $GPOPath + #$ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation + $Count = 0 + $GPOSummary = foreach ($GPO in $GPOs) { + #$QueryServer = $ForestInformation['QueryServers'][$GPO.Domain]['HostName'][0] + if ($Type -eq 'Empty') { + if ($GPO.ComputerSettingsAvailable -eq $false -and $GPO.UserSettingsAvailable -eq $false) { + Write-Verbose "Remove-GPOZaurr - Removing GPO $($GPO.Name) from $($GPO.Domain)" + $Count++ + Remove-GPO -Domain $GPO.Domain -Name $GPO.Name #-Server $QueryServer + if ($LimitProcessing -eq $Count) { + break + } + } + } elseif ($Type -eq 'EmptyAndUnlinked') { + if ($GPO.ComputerSettingsAvailable -eq $false -and $GPO.UserSettingsAvailable -eq $false -or $Gpo.Linked -eq $false) { + Write-Verbose "Remove-GPOZaurr - Removing GPO $($GPO.Name) from $($GPO.Domain)" + $Count++ + Remove-GPO -Domain $GPO.Domain -Name $GPO.Name #-Server $QueryServer + if ($LimitProcessing -eq $Count) { + break + } + } + } elseif ($Type -eq 'Unlinked') { + if ($Gpo.Linked -eq $false) { + Write-Verbose "Remove-GPOZaurr - Removing GPO $($GPO.Name) from $($GPO.Domain)" + $Count++ + Remove-GPO -Domain $GPO.Domain -Name $GPO.Name #-Server $QueryServer + if ($LimitProcessing -eq $Count) { + break + } + } + } + } + $GPOSummary +} \ No newline at end of file diff --git a/Public/Restore-GPOZaurr.ps1 b/Public/Restore-GPOZaurr.ps1 new file mode 100644 index 0000000..c03391d --- /dev/null +++ b/Public/Restore-GPOZaurr.ps1 @@ -0,0 +1,64 @@ +function Restore-GPOZaurr { + [cmdletBinding()] + param( + [parameter(Mandatory)][string] $BackupFolder, + [alias('Name')][string] $DisplayName, + [string] $NewDisplayName, + [string] $Domain, + [switch] $SkipBackupSummary + ) + if ($BackupFolder) { + if (Test-Path -LiteralPath $BackupFolder) { + if ($DisplayName) { + if (-not $SkipBackupSummary) { + $BackupSummary = Get-GPOZaurrBackupInformation -BackupFolder $BackupFolder + if ($Domain) { + [Array] $FoundGPO = $BackupSummary | Where-Object { $_.DisplayName -eq $DisplayName -and $_.Domain -eq $Domain } + } else { + [Array] $FoundGPO = $BackupSummary | Where-Object { $_.DisplayName -eq $DisplayName } + } + foreach ($GPO in $FoundGPO) { + if ($NewDisplayName) { + Import-GPO -Path $BackupFolder -BackupID $GPO.ID -Domain $GPO.Domain -TargetName $NewDisplayName -CreateIfNeeded + } else { + Write-Verbose "Restore-GPOZaurr - Restoring GPO $($GPO.DisplayName) from $($GPO.Domain) / BackupId: $($GPO.ID)" + try { + Restore-GPO -Path $BackupFolder -BackupID $GPO.ID -Domain $GPO.Domain + } catch { + Write-Warning "Restore-GPOZaurr - Restoring GPO $($GPO.DisplayName) from $($_.Domain) failed: $($_.Exception.Message)" + } + } + } + } else { + if ($Domain) { + Write-Verbose "Restore-GPOZaurr - Restoring GPO $($Name) from $($Domain)" + try { + Restore-GPO -Path $BackupFolder -Name $Name -Domain $Domain + } catch { + Write-Warning "Restore-GPOZaurr - Restoring GPO $($Name) from $($Domain) failed: $($_.Exception.Message)" + } + } else { + Write-Verbose "Restore-GPOZaurr - Restoring GPO $($Name)" + try { + Restore-GPO -Path $BackupFolder -Name $Name + } catch { + Write-Warning "Restore-GPOZaurr - Restoring GPO $($Name) failed: $($_.Exception.Message)" + } + } + } + } else { + $BackupSummary = Get-GPOZaurrBackupInformation -BackupFolder $BackupFolder + foreach ($GPO in $BackupSummary) { + Write-Verbose "Restore-GPOZaurr - Restoring GPO $($GPO.DisplayName) from $($GPO.Domain) / BackupId: $($GPO.ID)" + try { + Restore-GPO -Path $BackupFolder -Domain $GPO.Domain -BackupId $GPO.ID + } catch { + Write-Warning "Restore-GPOZaurr - Restoring GPO $($GPO.DisplayName) from $($GPO.Domain) failed: $($_.Exception.Message)" + } + } + } + } else { + Write-Warning "Restore-GPOZaurr - BackupFolder incorrect ($BackupFolder)" + } + } +} \ No newline at end of file diff --git a/Public/Save-GPOZaurrFiles.ps1 b/Public/Save-GPOZaurrFiles.ps1 index 4bad711..2db3ba7 100644 --- a/Public/Save-GPOZaurrFiles.ps1 +++ b/Public/Save-GPOZaurrFiles.ps1 @@ -5,7 +5,8 @@ [string[]] $ExcludeDomains, [alias('Domain', 'Domains')][string[]] $IncludeDomains, [System.Collections.IDictionary] $ExtendedForestInformation, - [string[]] $GPOPath + [string[]] $GPOPath, + [switch] $DeleteExisting ) if ($GPOPath) { if (-not $ExtendedForestInformation) { @@ -13,8 +14,18 @@ } else { $ForestInformation = $ExtendedForestInformation } + + if ($DeleteExisting) { + $Test = Test-Path -LiteralPath $GPOPath + if ($Test) { + Write-Verbose "Save-GPOZaurrFiles - Removing existing content in $GPOPath" + Remove-Item -LiteralPath $GPOPath -Recurse + } + } + $null = New-Item -ItemType Directory -Path $GPOPath -Force foreach ($Domain in $ForestInformation.Domains) { + Write-Verbose "Save-GPOZaurrFiles - Processing GPO for $Domain" Get-GPO -All -Server $ForestInformation.QueryServers[$Domain].HostName[0] -Domain $Domain | ForEach-Object { $XMLContent = Get-GPOReport -ID $_.ID.Guid -ReportType XML -Server $ForestInformation.QueryServers[$Domain].HostName[0] -Domain $Domain $Path = [io.path]::Combine($GPOPath, "$($_.ID.Guid).xml")