├── .gitattributes ├── .gitignore ├── GPOTools ├── GPOTools.psd1 ├── GPOTools.psm1 ├── bin │ └── readme.md ├── changelog.md ├── en-us │ ├── about_GPOTools.help.txt │ └── strings.psd1 ├── functions │ ├── Backup-GptPolicy.ps1 │ ├── Export-GptDomainData.ps1 │ ├── Export-GptIdentity.ps1 │ ├── Export-GptLink.ps1 │ ├── Export-GptObject.ps1 │ ├── Export-GptPermission.ps1 │ ├── Export-GptWmiFilter.ps1 │ ├── Get-GptPrincipal.ps1 │ ├── Import-GptDomainData.ps1 │ ├── Import-GptIdentity.ps1 │ ├── Import-GptLink.ps1 │ ├── Import-GptObject.ps1 │ ├── Import-GptPermission.ps1 │ ├── Import-GptWmiFilter.ps1 │ ├── Register-GptDomainMapping.ps1 │ ├── Restore-GptPolicy.ps1 │ └── readme.md ├── internal │ ├── configurations │ │ ├── configuration.ps1 │ │ └── readme.md │ ├── functions │ │ ├── ConvertFrom-ImportedIdentity.ps1 │ │ ├── ConvertTo-DnsDomainName.ps1 │ │ ├── Get-DomainData.ps1 │ │ ├── New-ImportResult.ps1 │ │ ├── New-MigrationTable.ps1 │ │ ├── Resolve-ADPrincipal.ps1 │ │ ├── Resolve-DomainMapping.ps1 │ │ ├── Test-IsDistinguishedName.ps1 │ │ ├── Test-Overlap.ps1 │ │ ├── Update-NetworkDrive.ps1 │ │ └── readme.md │ ├── scriptblocks │ │ └── scriptblocks.ps1 │ ├── scripts │ │ ├── license.ps1 │ │ ├── postimport.ps1 │ │ ├── preimport.ps1 │ │ └── strings.ps1 │ └── tepp │ │ ├── assignment.ps1 │ │ ├── example.tepp.ps1 │ │ └── readme.md ├── readme.md ├── tests │ ├── functions │ │ └── readme.md │ ├── general │ │ ├── FileIntegrity.Exceptions.ps1 │ │ ├── FileIntegrity.Tests.ps1 │ │ ├── Help.Exceptions.ps1 │ │ ├── Help.Tests.ps1 │ │ ├── Manifest.Tests.ps1 │ │ └── PSScriptAnalyzer.Tests.ps1 │ ├── pester.ps1 │ └── readme.md └── xml │ ├── GPOTools.Format.ps1xml │ ├── GPOTools.Types.ps1xml │ └── readme.md ├── LICENSE ├── README.md ├── azFunctionResources ├── clientModule │ ├── function.ps1 │ ├── functions │ │ └── Connect-GPOTools.ps1 │ ├── internal │ │ ├── configurations │ │ │ └── connection.ps1 │ │ └── functions │ │ │ └── Get-InternalConnectionData.ps1 │ └── moduleroot.psm1 ├── functionOverride │ ├── Get-Example.json │ ├── Get-Example.ps1 │ └── Get-Example.psd1 ├── host.json ├── local.settings.json ├── profile.ps1 ├── profileFunctions │ ├── Convert-AzureFunctionParameter.ps1 │ ├── Set-AzureFunctionStatus.ps1 │ └── Write-AzureFunctionOutput.ps1 ├── readme.md └── run.ps1 ├── build ├── AzureFunction.readme.md ├── filesAfter.txt ├── filesBefore.txt ├── vsts-build.ps1 ├── vsts-createFunctionClientModule.ps1 ├── vsts-packageFunction.ps1 ├── vsts-prerequisites.ps1 └── vsts-validate.ps1 ├── install.ps1 ├── labdefinitions └── gpoMigration.lab.ps1 └── library └── GPOTools ├── GPOTools.sln └── GPOTools ├── Class1.cs └── GPOTools.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 |  2 | # ignore the settings folder and files for VSCode and PSS 3 | .vscode/* 4 | *.psproj 5 | *TempPoint* 6 | 7 | # Ignore staging info from Visual Studio 8 | library/GPOTools/.vs/* 9 | library/GPOTools/GPOTools/bin/* 10 | library/GPOTools/GPOTools/obj/* 11 | 12 | # ignore PowerShell Studio MetaData 13 | GPOTools/GPOTools.psproj 14 | GPOTools/GPOTools.psproj.bak 15 | GPOTools/GPOTools.psprojs 16 | GPOTools/GPOTools.psproj 17 | 18 | # ignore the TestResults 19 | TestResults/* 20 | 21 | # ignore the publishing Directory 22 | publish/* -------------------------------------------------------------------------------- /GPOTools/GPOTools.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | # Script module or binary module file associated with this manifest 3 | RootModule = 'GPOTools.psm1' 4 | 5 | # Version number of this module. 6 | ModuleVersion = '0.3.0' 7 | 8 | # ID used to uniquely identify this module 9 | GUID = '8f9c10fa-1b99-4dc8-b6ed-1ec96e48e23c' 10 | 11 | # Author of this module 12 | Author = 'Friedrich Weinmann' 13 | 14 | # Company or vendor of this module 15 | CompanyName = 'Microsoft' 16 | 17 | # Copyright statement for this module 18 | Copyright = 'Copyright (c) 2019 Friedrich Weinmann' 19 | 20 | # Description of the functionality provided by this module 21 | Description = 'Tools for GPO Management & Migration' 22 | 23 | # Minimum version of the Windows PowerShell engine required by this module 24 | PowerShellVersion = '5.1' 25 | 26 | # Modules that must be imported into the global environment prior to importing 27 | # this module 28 | RequiredModules = @( 29 | #@{ ModuleName='PSFramework'; ModuleVersion='1.0.19' } 30 | ) 31 | 32 | # Assemblies that must be loaded prior to importing this module 33 | # RequiredAssemblies = @('bin\GPOTools.dll') 34 | 35 | # Type files (.ps1xml) to be loaded when importing this module 36 | # TypesToProcess = @('xml\GPOTools.Types.ps1xml') 37 | 38 | # Format files (.ps1xml) to be loaded when importing this module 39 | FormatsToProcess = @('xml\GPOTools.Format.ps1xml') 40 | 41 | # Functions to export from this module 42 | FunctionsToExport = @( 43 | 'Backup-GptPolicy' 44 | 'Export-GptDomainData' 45 | 'Export-GptIdentity' 46 | 'Export-GptLink' 47 | 'Export-GptObject' 48 | 'Export-GptPermission' 49 | 'Export-GptWmiFilter' 50 | 'Get-GptPrincipal' 51 | 'Import-GptDomainData' 52 | 'Import-GptIdentity' 53 | 'Import-GptLink' 54 | 'Import-GptObject' 55 | 'Import-GptPermission' 56 | 'Import-GptWmiFilter' 57 | 'Register-GptDomainMapping' 58 | 'Restore-GptPolicy' 59 | ) 60 | 61 | # Cmdlets to export from this module 62 | CmdletsToExport = @() 63 | 64 | # Variables to export from this module 65 | VariablesToExport = @() 66 | 67 | # Aliases to export from this module 68 | AliasesToExport = @() 69 | 70 | # List of all modules packaged with this module 71 | ModuleList = @() 72 | 73 | # List of all files packaged with this module 74 | FileList = @() 75 | 76 | # Private data to pass to the module specified in ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 77 | PrivateData = @{ 78 | 79 | #Support for PowerShellGet galleries. 80 | PSData = @{ 81 | 82 | # Tags applied to this module. These help with module discovery in online galleries. 83 | # Tags = @() 84 | 85 | # A URL to the license for this module. 86 | # LicenseUri = '' 87 | 88 | # A URL to the main website for this project. 89 | # ProjectUri = '' 90 | 91 | # A URL to an icon representing this module. 92 | # IconUri = '' 93 | 94 | # ReleaseNotes of this module 95 | # ReleaseNotes = '' 96 | 97 | } # End of PSData hashtable 98 | 99 | } # End of PrivateData hashtable 100 | } -------------------------------------------------------------------------------- /GPOTools/GPOTools.psm1: -------------------------------------------------------------------------------- 1 | $script:ModuleRoot = $PSScriptRoot 2 | $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\GPOTools.psd1").ModuleVersion 3 | 4 | # Detect whether at some level dotsourcing was enforced 5 | #$script:doDotSource = Get-PSFConfigValue -FullName GPOTools.Import.DoDotSource -Fallback $false 6 | $script:doDotSource = $false 7 | if ($GPOTools_dotsourcemodule) { $script:doDotSource = $true } 8 | 9 | <# 10 | Note on Resolve-Path: 11 | All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. 12 | This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. 13 | Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. 14 | This is important when testing for paths. 15 | #> 16 | 17 | # Detect whether at some level loading individual module files, rather than the compiled module was enforced 18 | #$importIndividualFiles = Get-PSFConfigValue -FullName GPOTools.Import.IndividualFiles -Fallback $false 19 | $importIndividualFiles = $false 20 | if ($GPOTools_importIndividualFiles) { $importIndividualFiles = $true } 21 | if (Test-Path "$($script:ModuleRoot)\..\.git") { $importIndividualFiles = $true } 22 | if ("" -eq '') { $importIndividualFiles = $true } 23 | 24 | function Import-ModuleFile 25 | { 26 | <# 27 | .SYNOPSIS 28 | Loads files into the module on module import. 29 | 30 | .DESCRIPTION 31 | This helper function is used during module initialization. 32 | It should always be dotsourced itself, in order to proper function. 33 | 34 | This provides a central location to react to files being imported, if later desired 35 | 36 | .PARAMETER Path 37 | The path to the file to load 38 | 39 | .EXAMPLE 40 | PS C:\> . Import-ModuleFile -File $function.FullName 41 | 42 | Imports the file stored in $function according to import policy 43 | #> 44 | [CmdletBinding()] 45 | Param ( 46 | [string] 47 | $Path 48 | ) 49 | 50 | $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath 51 | if ($doDotSource) { . $resolvedPath } 52 | else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } 53 | } 54 | 55 | #region Load individual files 56 | if ($importIndividualFiles) 57 | { 58 | # Execute Preimport actions 59 | . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1" 60 | 61 | # Import all internal functions 62 | foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) 63 | { 64 | . Import-ModuleFile -Path $function.FullName 65 | } 66 | 67 | # Import all public functions 68 | foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) 69 | { 70 | . Import-ModuleFile -Path $function.FullName 71 | } 72 | 73 | # Execute Postimport actions 74 | . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1" 75 | 76 | # End it here, do not load compiled code below 77 | return 78 | } 79 | #endregion Load individual files 80 | 81 | #region Load compiled code 82 | "" 83 | #endregion Load compiled code -------------------------------------------------------------------------------- /GPOTools/bin/readme.md: -------------------------------------------------------------------------------- 1 | # bin folder 2 | 3 | The bin folder exists to store binary data. And scripts related to the type system. 4 | 5 | This may include your own C#-based library, third party libraries you want to include (watch the license!), or a script declaring type accelerators (effectively aliases for .NET types) 6 | 7 | For more information on Type Accelerators, see the help on Set-PSFTypeAlias -------------------------------------------------------------------------------- /GPOTools/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## ??? 4 | 5 | + Fix: Register-GptDomainMapping - fails due to parameter error when splatting to Get-ADDomain (#9, @luzkenin) 6 | 7 | ## 0.3.0 (2020-07-05) 8 | 9 | + Upd: Network Share remapping 10 | + Fix: Identity resolution - Now works in Subdomains 11 | + Fix: Identity resolution - various issues 12 | 13 | ## 0.1.0 (2019-06-29) 14 | 15 | + Initial release 16 | -------------------------------------------------------------------------------- /GPOTools/en-us/about_GPOTools.help.txt: -------------------------------------------------------------------------------- 1 | TOPIC 2 | about_GPOTools 3 | 4 | SHORT DESCRIPTION 5 | Explains how to use the GPOTools powershell module 6 | 7 | LONG DESCRIPTION 8 | 9 | 10 | KEYWORDS 11 | GPOTools -------------------------------------------------------------------------------- /GPOTools/en-us/strings.psd1: -------------------------------------------------------------------------------- 1 | # This is where the strings go, that are written by 2 | # Write-PSFMessage, Stop-PSFFunction or the PSFramework validation scriptblocks 3 | @{ 4 | 'key' = 'Value' 5 | } -------------------------------------------------------------------------------- /GPOTools/functions/Backup-GptPolicy.ps1: -------------------------------------------------------------------------------- 1 | function Backup-GptPolicy 2 | { 3 | <# 4 | .SYNOPSIS 5 | Creates a full backup of all specified GPOs. 6 | 7 | .DESCRIPTION 8 | Creates a full backup of all specified GPOs. 9 | This includes permissions, settings, GPO Links and WMI Filter. 10 | 11 | .PARAMETER Path 12 | The path to the folder to export into. 13 | Folder must exist. 14 | 15 | .PARAMETER Name 16 | Filter Policy Objects by policy name. 17 | By default, ALL policies are targeted. 18 | 19 | .PARAMETER GpoObject 20 | Specify explicitly which GPOs to export. 21 | Accepts output of Get-GPO 22 | 23 | .PARAMETER Domain 24 | The source domain to export from. 25 | 26 | .PARAMETER Identity 27 | Additional identities to export. 28 | Identites are names of groups that are used for matching groups when importing policies. 29 | 30 | .EXAMPLE 31 | PS C:\> Backup-GptPolicy -Path . 32 | 33 | Export all policies to file. 34 | #> 35 | [CmdletBinding()] 36 | param ( 37 | [Parameter(Mandatory = $true)] 38 | [string] 39 | $Path, 40 | 41 | [string] 42 | $Name = '*', 43 | 44 | [Parameter(ValueFromPipeline = $true)] 45 | $GpoObject, 46 | 47 | [string] 48 | $Domain = $env:USERDNSDOMAIN, 49 | 50 | [string[]] 51 | $Identity 52 | ) 53 | 54 | begin 55 | { 56 | $resolvedPath = (Resolve-Path -Path $Path).ProviderPath 57 | $policyFolder = New-Item -Path $resolvedPath -Name GPO -ItemType Directory -Force 58 | Write-Verbose "Resolved output path to: $resolvedPath" 59 | 60 | $gpoObjects = @() 61 | } 62 | process 63 | { 64 | Write-Verbose "Resolving GPOs to process" 65 | if (-not $GpoObject) 66 | { 67 | $gpoObjects = Get-GPO -All -Domain $Domain | Where-Object DisplayName -Like $Name 68 | } 69 | else 70 | { 71 | foreach ($object in $GpoObject) 72 | { 73 | $gpoObjects += $object 74 | } 75 | } 76 | } 77 | end 78 | { 79 | Write-Verbose "Exporting GPO Objects" 80 | $gpoObjects | Export-GptObject -Path $policyFolder.FullName -Domain $Domain 81 | Write-Verbose "Exporting GP Links" 82 | Export-GptLink -Path $resolvedPath -Domain $Domain 83 | Write-Verbose "Exporting GP Permissions" 84 | $gpoObjects | Export-GptPermission -Path $resolvedPath -Domain $Domain 85 | Write-Verbose "Exporting WMI Filters" 86 | $gpoObjects | Export-GptWmiFilter -Path $resolvedPath -Domain $Domain 87 | Write-Verbose "Exporting Identities" 88 | Export-GptIdentity -Path $resolvedPath -Domain $Domain -Name $Identity -GpoObject $gpoObjects 89 | Write-Verbose "Exporting Domain Information" 90 | Export-GptDomainData -Path $resolvedPath -Domain $Domain 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /GPOTools/functions/Export-GptDomainData.ps1: -------------------------------------------------------------------------------- 1 | function Export-GptDomainData 2 | { 3 | <# 4 | .SYNOPSIS 5 | Generates a summary export of the source domain. 6 | 7 | .DESCRIPTION 8 | Generates a summary export of the source domain. 9 | This data is required or useful in several import stages. 10 | 11 | .PARAMETER Path 12 | The path to export to. 13 | Point at an existing folder. 14 | 15 | .PARAMETER Domain 16 | The domain to export the info of. 17 | 18 | .EXAMPLE 19 | PS C:\> Export-GptDomainData -Path '.' 20 | 21 | Exports the current domain's basic info into the current folder. 22 | #> 23 | [CmdletBinding()] 24 | Param ( 25 | [Parameter(Mandatory = $true)] 26 | [string] 27 | $Path, 28 | 29 | [string] 30 | $Domain = $env:USERDNSDOMAIN 31 | ) 32 | 33 | begin 34 | { 35 | $resolvedPath = (Resolve-Path -Path $Path).ProviderPath 36 | } 37 | process 38 | { 39 | $domainObject = Get-ADDomain -Server $Domain 40 | $sourceDomain = [pscustomobject]@{ 41 | Domain = $Domain 42 | DomainDNSName = $domainObject.DNSRoot 43 | NetBIOSName = $domainObject.NetBIOSName 44 | BackupVersion = '1.0.0' 45 | Timestamp = (Get-Date) 46 | DomainSID = $domainObject.DomainSID.Value 47 | } 48 | 49 | $forestObject = Get-ADForest -Server $Domain 50 | $domains = $forestObject.Domains | Foreach-Object { Get-ADDomain -Server $_ -Identity $_ } | ForEach-Object { 51 | [PSCustomObject]@{ 52 | DistinguishedName = $_.DistinguishedName 53 | Name = $_.Name 54 | SID = $_.DomainSID 55 | Fqdn = $_.DNSRoot 56 | ADObject = $_ 57 | IsTarget = $_.DomainSID -eq $sourceDomain.DomainSID 58 | IsRootDomain = $_.DNSRoot -eq $forestObject.RootDomain 59 | } 60 | } 61 | 62 | [PSCustomObject]@{ 63 | SourceDomain = $sourceDomain 64 | ForestDomains = $domains 65 | } | Export-Clixml -Path (Join-Path -Path $resolvedPath -ChildPath 'backup.clixml') 66 | } 67 | } -------------------------------------------------------------------------------- /GPOTools/functions/Export-GptIdentity.ps1: -------------------------------------------------------------------------------- 1 | function Export-GptIdentity 2 | { 3 | <# 4 | .SYNOPSIS 5 | Exports identity data used for Group Policy imports. 6 | 7 | .DESCRIPTION 8 | Generates an export dump of identity information. 9 | This is later used during import of group policy objects: 10 | - To map between identities for permissions and policy content. 11 | - To translate localized builtin account names. 12 | - To correctly target renamed builtin acconts. 13 | 14 | .PARAMETER Path 15 | The path where the exprot should be stored in. 16 | Specify an existing folder. 17 | 18 | .PARAMETER Name 19 | Names of groups to include in addition to the builtin accounts. 20 | 21 | .PARAMETER Domain 22 | The domain to generate the dump from. 23 | 24 | .PARAMETER GpoName 25 | The name filter pattern of the GPOs to parse for relevant identities export. 26 | 27 | .PARAMETER GpoObject 28 | Specific GPO object to parse for relevant identities to export. 29 | 30 | .EXAMPLE 31 | PS C:\> Export-GptIdentity -Path '.' 32 | 33 | Export the builtin accounts into the current folder. 34 | #> 35 | [CmdletBinding()] 36 | param ( 37 | [Parameter(Mandatory = $true)] 38 | [string] 39 | $Path, 40 | 41 | [string[]] 42 | $Name, 43 | 44 | [string[]] 45 | $GpoName = '*', 46 | 47 | [Parameter(ValueFromPipeline = $true)] 48 | $GpoObject, 49 | 50 | [string] 51 | $Domain = $env:USERDNSDOMAIN 52 | ) 53 | 54 | begin 55 | { 56 | $pdcEmulator = (Get-ADDomain -Server $Domain).PDCEmulator 57 | $rootDomain = Get-ADDomain (Get-ADForest -Server $Domain).RootDomain 58 | 59 | [System.Collections.ArrayList]$identities = @() 60 | 61 | #region Process Builtin Accounts 62 | $builtInSID = 'S-1-5-32-544', 'S-1-5-32-545', 'S-1-5-32-546', 'S-1-5-32-548', 'S-1-5-32-549', 'S-1-5-32-550', 'S-1-5-32-551', 'S-1-5-32-552', 'S-1-5-32-554', 'S-1-5-32-555', 'S-1-5-32-556', 'S-1-5-32-557', 'S-1-5-32-558', 'S-1-5-32-559', 'S-1-5-32-560', 'S-1-5-32-561', 'S-1-5-32-562', 'S-1-5-32-568', 'S-1-5-32-569', 'S-1-5-32-573', 'S-1-5-32-574', 'S-1-5-32-575', 'S-1-5-32-576', 'S-1-5-32-577', 'S-1-5-32-578', 'S-1-5-32-579', 'S-1-5-32-580', 'S-1-5-32-582' 63 | $builtInRID = '500', '501', '502', '512', '513', '514', '515', '516', '517','520', '521', '522', '525', '526', '553', '571', '572' 64 | $builtInForestRID = @( 65 | '498' # Enterprise Read-only Domain Controllers 66 | '518' # Schema Admins 67 | '519' # Enterprise Admins 68 | '527' # Enterprise Key Admins 69 | ) 70 | $domainSID = (Get-ADDomain -Server $pdcEmulator).DomainSID.Value 71 | $rootDomainSID = $rootDomain.DomainSID.Value 72 | $identities.AddRange(($builtInSID | Resolve-ADPrincipal -Domain $Domain)) 73 | $identities.AddRange(($builtInRID | Resolve-ADPrincipal -Domain $Domain -Name { '{0}-{1}' -f $domainSID, $_ })) 74 | $identities.AddRange(($builtInForestRID | Resolve-ADPrincipal -Domain $rootDomain.DNSRoot -Name { '{0}-{1}' -f $rootDomainSID, $_ })) 75 | #endregion Process Builtin Accounts 76 | 77 | #region Process Additional Requested Accounts 78 | foreach ($adEntity in $Name) 79 | { 80 | #region Handle Wildcard Filters 81 | if ($adEntity.Contains("*")) 82 | { 83 | $identities.AddRange((Get-ADGroup -Server $pdcEmulator -LDAPFilter "(name=$adEntity)" | Resolve-ADPrincipal -Domain $Domain)) 84 | continue 85 | } 86 | #endregion Handle Wildcard Filters 87 | try 88 | { 89 | $principal = Resolve-ADPrincipal -Name $adEntity -Domain $Domain -ErrorAction Stop 90 | $null = $identities.Add($principal) 91 | } 92 | catch { Write-Error -Message "Failed to resolve Identity: $adEntity | $_" -Exception $_.Exception } 93 | } 94 | #endregion Process Additional Requested Accounts 95 | } 96 | process 97 | { 98 | #region Process GPO-Required Accounts 99 | foreach ($gpoItem in $GpoObject) { 100 | foreach ($principal in (Get-GptPrincipal -Name $GpoName -GpoObject $GpoObject -Domain $Domain)) { 101 | $null = $identities.Add($principal) 102 | } 103 | } 104 | #endregion Process GPO-Required Accounts 105 | } 106 | end 107 | { 108 | $identities | Group-Object SID | ForEach-Object { 109 | $_.Group | Select-Object -First 1 110 | } | Export-Csv -Path (Join-Path -Path $Path -ChildPath "gp_Identities_$($Domain).csv") -Encoding UTF8 -NoTypeInformation 111 | } 112 | } -------------------------------------------------------------------------------- /GPOTools/functions/Export-GptLink.ps1: -------------------------------------------------------------------------------- 1 | function Export-GptLink 2 | { 3 | <# 4 | .SYNOPSIS 5 | Generates a full dump of all GPO links. 6 | 7 | .DESCRIPTION 8 | Generates a full dump of all GPO links. 9 | This command will enumerate all OUs and create an export file of them. 10 | This is used to restore links of exported GPOs when restoring them. 11 | 12 | .PARAMETER Path 13 | The path in which to export the data. 14 | Specify an existing folder. 15 | 16 | .PARAMETER Domain 17 | The domain to retrieve the data from. 18 | 19 | .EXAMPLE 20 | PS C:\> Export-GptLink -Path . 21 | 22 | Exports all GPO links into the current folder. 23 | #> 24 | [CmdletBinding()] 25 | param ( 26 | [Parameter(Mandatory = $true)] 27 | [string] 28 | $Path, 29 | 30 | [string] 31 | $Domain = $env:USERDNSDOMAIN 32 | ) 33 | 34 | begin 35 | { 36 | $gpoObjects = Get-GPO -All -Domain $Domain 37 | } 38 | process 39 | { 40 | Get-ADOrganizationalUnit -Server $Domain -LdapFilter '(gpLink=*)' -Properties gpLink, CanonicalName | ForEach-Object { 41 | $indexCount = 0 42 | $links = $_.gpLink -replace '\]\[', ']_[' -split '_' 43 | foreach ($link in $links) 44 | { 45 | # Skip empty lines 46 | if (-not $link) { continue } 47 | $path, $state = $link -replace '\[LDAP://' -replace '\]$' -split ';' 48 | [PSCustomObject]@{ 49 | Path = $Path 50 | State = $state # 0: Normal, 1: Disabled, 2: Enforced 51 | GpoName = ($gpoObjects | Where-Object Path -EQ $path).DisplayName 52 | Domain = $Domain 53 | OUDN = $_.DistinguishedName 54 | OUName = $_.Name 55 | OUCanonical = $_.CanonicalName 56 | Index = $indexCount++ 57 | TotalCount = $links.Count 58 | } 59 | } 60 | } | Export-Csv -Path (Join-Path -Path $Path -ChildPath "gp_Links_$($Domain).csv") -Encoding UTF8 -NoTypeInformation 61 | } 62 | } -------------------------------------------------------------------------------- /GPOTools/functions/Export-GptObject.ps1: -------------------------------------------------------------------------------- 1 | function Export-GptObject 2 | { 3 | <# 4 | .SYNOPSIS 5 | Creates a backup of all specified GPOs. 6 | 7 | .DESCRIPTION 8 | Creates a backup of all specified GPOs. 9 | 10 | .PARAMETER Path 11 | The path in which to generate the Backup. 12 | 13 | .PARAMETER Name 14 | The name to filter GPOs by. 15 | By default, ALL GPOs are exported. 16 | 17 | .PARAMETER GpoObject 18 | Select the GPOs to export by specifying the explicit GPO object to export. 19 | 20 | .PARAMETER Domain 21 | The domain from which to export the GPOs 22 | 23 | .EXAMPLE 24 | PS C:\> Export-GptObject -Path . 25 | 26 | Generate a GPO export of all GPOs in the current folder. 27 | #> 28 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] 29 | [CmdletBinding()] 30 | param ( 31 | [Parameter(Mandatory = $true)] 32 | [string] 33 | $Path, 34 | 35 | [string[]] 36 | $Name = '*', 37 | 38 | [Parameter(ValueFromPipeline = $true)] 39 | $GpoObject, 40 | 41 | [string] 42 | $Domain = $env:USERDNSDOMAIN 43 | ) 44 | 45 | process 46 | { 47 | $gpoObjects = $GpoObject | Where-Object { 48 | Test-Overlap -ReferenceObject $_.DisplayName -DifferenceObject $Name -Operator Like 49 | } 50 | if (-not $GpoObject) 51 | { 52 | $gpoObjects = Get-GPO -All -Domain $Domain | Where-Object { 53 | Test-Overlap -ReferenceObject $_.DisplayName -DifferenceObject $Name -Operator Like 54 | } 55 | } 56 | $null = $gpoObjects | Backup-GPO -Path (Resolve-Path $Path).ProviderPath 57 | $gpoObjects | Select-Object DisplayName, ID, Owner, CreationTime, ModificationTime, @{ Name = 'WmiFilter'; Expression = { $_.WmiFilter.Name }} | Export-Csv -Path (Join-Path -Path $Path -ChildPath "gp_object_$($Domain).csv") -Encoding UTF8 -NoTypeInformation -Append 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /GPOTools/functions/Export-GptPermission.ps1: -------------------------------------------------------------------------------- 1 | function Export-GptPermission 2 | { 3 | <# 4 | .SYNOPSIS 5 | Export the permissions assigned on GPOs 6 | 7 | .DESCRIPTION 8 | Export the permissions assigned on GPOs. 9 | 10 | Note: This command is currently fairly slow so give it some time. 11 | 12 | .PARAMETER Path 13 | The path where to create the export. 14 | Must be an existing folder. 15 | 16 | .PARAMETER Name 17 | Filter GPOs to process by name. 18 | 19 | .PARAMETER GpoObject 20 | Specify GPOs to process by object. 21 | 22 | .PARAMETER IncludeInherited 23 | Include inherited permissions in the export. 24 | By default, only explicit permissiosn are exported. 25 | Note: By default, all GPOs in a windows domain only have explicit permissions set. 26 | This will have little impact in most scenarios. 27 | 28 | .PARAMETER Domain 29 | The domain to export from. 30 | 31 | .EXAMPLE 32 | PS C:\> Export-GptPermission -Path '.' 33 | 34 | Exports permissions of all GPOs into the current folder. 35 | #> 36 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] 37 | [CmdletBinding()] 38 | Param ( 39 | [Parameter(Mandatory = $true)] 40 | [ValidateScript({ Test-Path -Path $_ })] 41 | [string] 42 | $Path, 43 | 44 | [string] 45 | $Name = '*', 46 | 47 | [Parameter(ValueFromPipeline = $true)] 48 | $GpoObject, 49 | 50 | [switch] 51 | $IncludeInherited, 52 | 53 | [string] 54 | $Domain = $env:USERDNSDOMAIN 55 | ) 56 | 57 | begin 58 | { 59 | Write-Verbose "Preparing Filters" 60 | $select_Name = @{ name = 'GpoName'; expression = { $gpoItem.DisplayName } } 61 | $select_Path = @{ name = 'GpoPath'; expression = { $gpoItem.Path } } 62 | $select_SID = @{ name = 'SID'; expression = { (Resolve-ADPrincipal -Name $_.IdentityReference -Domain $Domain).SID } } 63 | $select_RID = @{ name = 'RID'; expression = { (Resolve-ADPrincipal -Name $_.IdentityReference -Domain $Domain).RID } } 64 | $select_IsBuiltin = @{ name = 'IsBuiltIn'; expression = { (Resolve-ADPrincipal -Name $_.IdentityReference -Domain $Domain).IsBuiltIn } } 65 | $select_PrincipalType = @{ name = 'PrincipalType'; expression = { (Resolve-ADPrincipal -Name $_.IdentityReference -Domain $Domain).Type } } 66 | $select_DomainFqdn = @{ name = 'DomainFqdn'; expression = { (Resolve-ADPrincipal -Name $_.IdentityReference -Domain $Domain).DomainFqdn } } 67 | $select_DomainName = @{ name = 'DomainName'; expression = { (Resolve-ADPrincipal -Name $_.IdentityReference -Domain $Domain).DomainName } } 68 | 69 | [System.Collections.ArrayList]$accessList = @() 70 | } 71 | process 72 | { 73 | Write-Verbose "Resolving Policies to process" 74 | $gpoObjects = $GpoObject 75 | if (-not $GpoObject) 76 | { 77 | $gpoObjects = Get-GPO -All -Domain $Domain | Where-Object DisplayName -Like $Name 78 | } 79 | Write-Verbose "Found $($gpoObjects.Count) Policies" 80 | $accessData = foreach ($gpoItem in $gpoObjects) 81 | { 82 | Write-Verbose "Processing policy: $($gpoItem.DisplayName)" 83 | $adObject = Get-ADObject -Identity $gpoItem.Path -Server $gpoItem.DomainName -Properties ntSecurityDescriptor 84 | $adObject.ntSecurityDescriptor.Access | Where-Object { 85 | $IncludeInherited -or -not $_.IsInherited 86 | } | Select-Object $select_Name, $select_Path, '*', $select_SID, $select_RID, $select_IsBuiltin, $select_PrincipalType, $select_DomainFqdn, $select_DomainName 87 | } 88 | Write-Verbose "Found $($accessData.Count) permission entries." 89 | $null = $accessList.AddRange($accessData) 90 | } 91 | end 92 | { 93 | Write-Verbose "Exorting to file" 94 | $accessList | Export-Csv -Path (Join-Path -Path $Path -ChildPath "gp_permissions_$($Domain).csv") -Encoding UTF8 -NoTypeInformation 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /GPOTools/functions/Export-GptWmiFilter.ps1: -------------------------------------------------------------------------------- 1 | function Export-GptWmiFilter { 2 | <# 3 | .SYNOPSIS 4 | Export WMI Filters. 5 | 6 | .DESCRIPTION 7 | Export WMI Filters. 8 | By default, all filters are exported. 9 | 10 | Use -ConstrainExport parameter to switch this behavior to: 11 | WMI Filters to export are picked up by the GPO they are assigned to. 12 | Unassigned filters are ignored. 13 | 14 | .PARAMETER Path 15 | The path where to create the export. 16 | Must be an existing folder. 17 | 18 | .PARAMETER ConstrainExport 19 | Don't export all WMI filters, instead: 20 | WMI Filters to export are picked up by the GPO they are assigned to. 21 | Unassigned filters are ignored. 22 | 23 | .PARAMETER Name 24 | Filter GPOs to process by name. 25 | 26 | .PARAMETER GpoObject 27 | Specify GPOs to process by object. 28 | 29 | .PARAMETER Domain 30 | The domain to export from. 31 | 32 | .EXAMPLE 33 | PS C:\> Export-GptWmiFilter -Path '.' 34 | 35 | Export all WMI Filters of all GPOs into the current folder. 36 | #> 37 | [CmdletBinding()] 38 | param ( 39 | [ValidateScript( { Test-Path -Path $_ })] 40 | [Parameter(Mandatory = $true)] 41 | [string] 42 | $Path, 43 | 44 | [switch] 45 | $ConstrainExport, 46 | 47 | [string] 48 | $Name = '*', 49 | 50 | [Parameter(ValueFromPipeline = $true)] 51 | $GpoObject, 52 | 53 | [string] 54 | $Domain = $env:USERDNSDOMAIN 55 | ) 56 | 57 | begin { 58 | $wmiPath = "CN=SOM,CN=WMIPolicy,$((Get-ADDomain -Server $Domain).SystemsContainer)" 59 | $allFilterHash = @{ } 60 | $foundFilterHash = @{ } 61 | 62 | Get-ADObject -Server $Domain -SearchBase $wmiPath -Filter { objectClass -eq 'msWMI-Som' } -Properties msWMI-Author, msWMI-Name, msWMI-Parm1, msWMI-Parm2 | ForEach-Object { 63 | $allFilterHash[$_.'msWMI-Name'] = [pscustomobject]@{ 64 | Author = $_.'msWMI-Author' 65 | Name = $_.'msWMI-Name' 66 | Description = $_.'msWMI-Parm1' 67 | Filter = $_.'msWMI-Parm2' 68 | } 69 | } 70 | } 71 | process { 72 | if (-not $ConstrainExport) { return } 73 | 74 | $gpoObjects = $GpoObject 75 | if (-not $GpoObject) { 76 | $gpoObjects = Get-GPO -All -Domain $Domain | Where-Object DisplayName -Like $Name 77 | } 78 | foreach ($filterName in $gpoObjects.WmiFilter.Name) { 79 | $foundFilterHash[$filterName] = $allFilterHash[$filterName] 80 | } 81 | } 82 | end { 83 | if ($ConstrainExport) { 84 | $foundFilterHash.Values | Where-Object { $_ } | Export-Csv -Path (Join-Path -Path $Path -ChildPath "gp_wmifilters_$($Domain).csv") -Encoding UTF8 -NoTypeInformation 85 | } 86 | else { 87 | $allFilterHash.Values | Where-Object { $_ } | Export-Csv -Path (Join-Path -Path $Path -ChildPath "gp_wmifilters_$($Domain).csv") -Encoding UTF8 -NoTypeInformation 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /GPOTools/functions/Get-GptPrincipal.ps1: -------------------------------------------------------------------------------- 1 | function Get-GptPrincipal 2 | { 3 | <# 4 | .SYNOPSIS 5 | Generates a list of principals relevant to the specified GPO. 6 | 7 | .DESCRIPTION 8 | Generates a list of principals relevant to the specified GPO. 9 | This is used internally to generate the identities export. 10 | It can also be used directly, to assess needed identities (for example when setting up a test domain). 11 | 12 | .PARAMETER Path 13 | Path to an already existing GPO backup. 14 | Using this will have the module scan a backup, rather than live GPO. 15 | 16 | .PARAMETER Name 17 | The name to filter GPOs by. 18 | Defaults to '*' 19 | Accepts multiple strings, a single wildcard match is needed for a GPO to be selected. 20 | 21 | .PARAMETER GpoObject 22 | The GPO to process, as returned by Get-Gpo. 23 | 24 | .PARAMETER Domain 25 | The domain to connect to. 26 | Defaults to the user dns domain. 27 | 28 | .PARAMETER IncludeUNC 29 | By default, UNC paths are not included in the output. 30 | These too can be read from GPO and might be relevant. 31 | 32 | .EXAMPLE 33 | PS C:\> Get-GptPrincipal 34 | 35 | Returns the relevant principals from all GPOs in the current domain. 36 | #> 37 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSPossibleIncorrectUsageOfAssignmentOperator', '')] 38 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] 39 | [CmdletBinding(DefaultParameterSetName = 'GPO')] 40 | param ( 41 | [Parameter(ParameterSetName = "Path")] 42 | [ValidateScript({ Test-Path -Path $_ })] 43 | [string] 44 | $Path, 45 | 46 | [Parameter(ParameterSetName = 'GPO')] 47 | [string[]] 48 | $Name = '*', 49 | 50 | [Parameter(ParameterSetName = 'GPO', ValueFromPipeline = $true)] 51 | $GpoObject, 52 | 53 | [string] 54 | $Domain = $env:USERDNSDOMAIN, 55 | 56 | [switch] 57 | $IncludeUNC 58 | ) 59 | 60 | begin 61 | { 62 | if (-not $Path) 63 | { 64 | $tempPath = New-Item -Path $env:TEMP -ItemType Directory -Name "Gpo_TempBackup_$(Get-Random -Maximum 999999 -Minimum 100000)" -Force 65 | $backupPath = $tempPath.FullName 66 | } 67 | else { $backupPath = (Resolve-Path -Path $Path).ProviderPath } 68 | 69 | $entryType = @{ 70 | 0 = 'User' 71 | 1 = 'Computer' 72 | 2 = 'LocalGroup' 73 | 3 = 'DomainGroup' 74 | 4 = 'UniversalGroup' 75 | 5 = 'UNCPath' 76 | 6 = 'Unknown' 77 | } 78 | } 79 | process 80 | { 81 | #region Export GPO to temporary path 82 | if (-not $Path) 83 | { 84 | $gpoObjects = $GpoObject | Where-Object { 85 | Test-Overlap -ReferenceObject $_.DisplayName -DifferenceObject $Name -Operator Like 86 | } 87 | if (-not $GpoObject) 88 | { 89 | $gpoObjects = Get-GPO -All -Domain $Domain | Where-Object { 90 | Test-Overlap -ReferenceObject $_.DisplayName -DifferenceObject $Name -Operator Like 91 | } 92 | } 93 | $null = $gpoObjects | Backup-GPO -Path $backupPath 94 | } 95 | #endregion Export GPO to temporary path 96 | } 97 | end 98 | { 99 | $groupPolicyManager = New-Object -ComObject GPMgmt.GPM 100 | $migrationTable = $groupPolicyManager.CreateMigrationTable() 101 | $constants = $groupPolicyManager.getConstants() 102 | $backupDirectory = $groupPolicyManager.GetBackupDir($backupPath) 103 | $backupList = $backupDirectory.SearchBackups($groupPolicyManager.CreateSearchCriteria()) 104 | 105 | foreach ($policyBackup in $backupList) 106 | { 107 | $migrationTable.Add(0, $policyBackup) 108 | $migrationTable.Add($constants.ProcessSecurity, $policyBackup) 109 | } 110 | 111 | foreach ($entry in $migrationTable.GetEntries()) 112 | { 113 | $paramAddMember = @{ 114 | MemberType = 'NoteProperty' 115 | Name = 'EntryType' 116 | Value = $entryType[$entry.EntryType] 117 | PassThru = $true 118 | Force = $true 119 | } 120 | 121 | switch ($entry.EntryType) 122 | { 123 | $constants.EntryTypeUNCPath 124 | { 125 | if (-not $IncludeUNC) { break } 126 | 127 | [PSCustomObject]@{ 128 | EntryType = $entryType[$entry.EntryType] 129 | Path = $entry.Source 130 | } 131 | } 132 | default 133 | { 134 | #region SID 135 | if ($sid = $entry.Source -as [System.Security.Principal.SecurityIdentifier]) 136 | { 137 | if ($sid.DomainSID) 138 | { 139 | Resolve-ADPrincipal -Name $sid -Domain $sid.DomainSID | Add-Member @paramAddMember 140 | continue 141 | } 142 | 143 | Resolve-ADPrincipal -Name $sid -Domain $Domain | Add-Member @paramAddMember 144 | continue 145 | } 146 | #endregion SID 147 | 148 | #region Name 149 | try 150 | { 151 | $sid = ([System.Security.Principal.NTAccount]$entry.Source).Translate([System.Security.Principal.SecurityIdentifier]) 152 | 153 | if ($sid.DomainSID) 154 | { 155 | Resolve-ADPrincipal -Name $sid -Domain $sid.DomainSID | Add-Member @paramAddMember 156 | continue 157 | } 158 | 159 | Resolve-ADPrincipal -Name $sid -Domain $Domain | Add-Member @paramAddMember 160 | continue 161 | } 162 | catch 163 | { 164 | if ($entry.Source -like '*@*') 165 | { 166 | $entity, $domainName = $entry.Source -split '@' 167 | Resolve-ADPrincipal -Name $entity -Domain $domainName | Add-Member @paramAddMember 168 | continue 169 | } 170 | else 171 | { 172 | Resolve-ADPrincipal -Name $entry.Source -Domain $Domain | Add-Member @paramAddMember 173 | continue 174 | } 175 | } 176 | #endregion Name 177 | } 178 | } 179 | } 180 | 181 | if (-not $Path) 182 | { 183 | Remove-Item -Path $tempPath -Recurse -Force 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /GPOTools/functions/Import-GptDomainData.ps1: -------------------------------------------------------------------------------- 1 | function Import-GptDomainData 2 | { 3 | <# 4 | .SYNOPSIS 5 | Imports domain information of the source domain. 6 | 7 | .DESCRIPTION 8 | Imports domain information of the source domain. 9 | Also responsible for mapping domains from the source forest to the destination forest. 10 | 11 | .PARAMETER Path 12 | The path to the file or the folder it resides in. 13 | 14 | .PARAMETER Domain 15 | The domain into which to import. 16 | Used for automatically calculating domain mappings. 17 | 18 | .EXAMPLE 19 | PS C:\> Import-GptDomainData -Path '.' 20 | 21 | Import the domain information file from the current folder. 22 | #> 23 | [CmdletBinding()] 24 | Param ( 25 | [Parameter(Mandatory = $true)] 26 | [string] 27 | $Path, 28 | 29 | [string] 30 | $Domain = $env:USERDNSDOMAIN 31 | ) 32 | 33 | begin 34 | { 35 | $pathItem = Get-Item -Path $Path 36 | if ($pathItem.Extension -eq '.clixml') { $resolvedPath = $pathItem.FullName } 37 | else { $resolvedPath = (Get-ChildItem -Path $pathItem.FullName -Filter 'backup.clixml' | Select-Object -First 1).FullName } 38 | if (-not $resolvedPath) { throw "Could not find a domain data file in $($pathItem.FullName)" } 39 | } 40 | process 41 | { 42 | $domainImport = Import-Clixml $resolvedPath 43 | $script:sourceDomainData = $domainImport.SourceDomain 44 | 45 | $forestObject = Get-ADForest -Server $Domain 46 | $targetDomain = Get-ADDomain -Server $Domain 47 | $domains = $forestObject.Domains | Foreach-Object { Get-ADDomain -Server $_ -Identity $_ } | ForEach-Object { 48 | [PSCustomObject]@{ 49 | DistinguishedName = $_.DistinguishedName 50 | Name = $_.Name 51 | SID = $_.DomainSID 52 | Fqdn = $_.DNSRoot 53 | ADObject = $_ 54 | IsTarget = $_.DomainSID -eq $targetDomain.DomainSID 55 | IsRootDomain = $_.DNSRoot -eq $forestObject.RootDomain 56 | } 57 | } 58 | 59 | foreach ($domainItem in $domains) { 60 | foreach ($sourceDomainEntry in $domainImport.ForestDomains) { 61 | if ($sourceDomainEntry.Name -eq $domainItem.Name) { 62 | Register-GptDomainMapping -SourceName $sourceDomainEntry.Name -SourceFQDN $sourceDomainEntry.Fqdn -SourceSID $sourceDomainEntry.SID -Destination $domainItem.ADObject 63 | } 64 | } 65 | } 66 | foreach ($domainItem in $domains) { 67 | foreach ($sourceDomainEntry in $domainImport.ForestDomains) { 68 | if ($sourceDomainEntry.Fqdn -eq $domainItem.Fqdn) { 69 | Register-GptDomainMapping -SourceName $sourceDomainEntry.Name -SourceFQDN $sourceDomainEntry.Fqdn -SourceSID $sourceDomainEntry.SID -Destination $domainItem.ADObject 70 | } 71 | } 72 | } 73 | foreach ($domainItem in $domains) { 74 | foreach ($sourceDomainEntry in $domainImport.ForestDomains) { 75 | if ($sourceDomainEntry.SID -eq $domainItem.SID) { 76 | Register-GptDomainMapping -SourceName $sourceDomainEntry.Name -SourceFQDN $sourceDomainEntry.Fqdn -SourceSID $sourceDomainEntry.SID -Destination $domainItem.ADObject 77 | } 78 | } 79 | } 80 | $sourceDomain = $domainImport.ForestDomains | Where-Object IsTarget 81 | $sourceForestRootDomain = $domainImport.ForestDomains | Where-Object IsRootDomain 82 | foreach ($domainItem in $domains) { 83 | if ($domainItem.IsRootDomain) { 84 | Register-GptDomainMapping -SourceName $sourceForestRootDomain.Name -SourceFQDN $sourceForestRootDomain.Fqdn -SourceSID $sourceForestRootDomain.SID -Destination $domainItem.ADObject 85 | } 86 | } 87 | foreach ($domainItem in $domains) { 88 | if ($domainItem.IsTarget) { 89 | Register-GptDomainMapping -SourceName $sourceDomain.Name -SourceFQDN $sourceDomain.Fqdn -SourceSID $sourceDomain.SID -Destination $domainItem.ADObject 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /GPOTools/functions/Import-GptIdentity.ps1: -------------------------------------------------------------------------------- 1 | function Import-GptIdentity 2 | { 3 | <# 4 | .SYNOPSIS 5 | Imports identity data exported from the source domain. 6 | 7 | .DESCRIPTION 8 | Imports identity data exported from the source domain. 9 | This data is used for mapping source identities to destination identities. 10 | 11 | .PARAMETER Path 12 | The path where to pick up the file. 13 | 14 | .PARAMETER Name 15 | Filter identities by name. 16 | 17 | .PARAMETER Domain 18 | The destination domain that later GPOs will be imported to. 19 | 20 | .PARAMETER Mapping 21 | A mapping hashtable allowing you to map identities that have unequal names. 22 | 23 | .EXAMPLE 24 | PS C:\> Import-GptIdentity -Path '.' 25 | 26 | Import the identity export file from the current folder. 27 | #> 28 | [CmdletBinding()] 29 | param ( 30 | [Parameter(Mandatory = $true)] 31 | [ValidateScript({ Test-Path -Path $_ })] 32 | [string] 33 | $Path, 34 | 35 | [string[]] 36 | $Name = '*', 37 | 38 | [string] 39 | $Domain = $env:USERDNSDOMAIN, 40 | 41 | [System.Collections.IDictionary] 42 | $Mapping = @{ } 43 | ) 44 | 45 | begin 46 | { 47 | $pathItem = Get-Item -Path $Path 48 | if ($pathItem.Extension -eq '.csv') { $resolvedPath = $pathItem.FullName } 49 | else { $resolvedPath = (Get-ChildItem -Path $pathItem.FullName -Filter 'gp_Identities*.csv' | Select-Object -First 1).FullName } 50 | if (-not $resolvedPath) { throw "Could not find identities file in $($pathItem.FullName)" } 51 | 52 | $rootDomain = (Get-ADForest -Server $Domain).RootDomain 53 | 54 | # Declare Module scope index of identities and what they map to 55 | $script:identityMapping = New-Object 'System.Collections.Generic.List[Object]' 56 | 57 | # Helpful Select Hashtables 58 | $select_TargetMapping = @{ 59 | Name = 'Target' 60 | Expression = { $Mapping[$importEntry.Name] } 61 | } 62 | $select_TargetName = @{ 63 | Name = 'Target' 64 | Expression = { $targetName } 65 | } 66 | $select_TargetDomain = @{ 67 | Name = 'TargetDomain' 68 | Expression = { $domainObject } 69 | } 70 | } 71 | process 72 | { 73 | $importData = Import-Csv -Path $resolvedPath 74 | foreach ($importEntry in $importData) 75 | { 76 | # Skip entries filtered out 77 | if (-not (Test-Overlap -ReferenceObject $importEntry.Name -DifferenceObject $Name -Operator Like)) 78 | { 79 | continue 80 | } 81 | 82 | #region Case: Mapped Entry 83 | if ($Mapping[$importEntry.Name]) 84 | { 85 | $script:identityMapping.Add(($importEntry | Select-Object *, $select_TargetMapping)) 86 | } 87 | #endregion Case: Mapped Entry 88 | 89 | #region Case: Discovery 90 | else 91 | { 92 | #region Case: Native BuiltIn Principal 93 | if (($importEntry.IsBuiltIn -eq 'True') -and ($importEntry.SID -like "*-32-*")) 94 | { 95 | try { $targetName = ([System.Security.Principal.SecurityIdentifier]$importEntry.SID).Translate([System.Security.Principal.NTAccount]).Value } 96 | catch 97 | { 98 | $adObject = Get-ADObject -Server $rootDomain -LDAPFilter "(objectSID=$($importEntry.SID))" -Properties Name 99 | if (-not $adObject) { 100 | Write-Warning "Failed to translate identity: $($importEntry.Name) ($($importEntry.SID))" 101 | continue 102 | } 103 | $targetName = $adObject.Name 104 | } 105 | $script:identityMapping.Add(($importEntry | Select-Object *, $select_TargetName)) 106 | } 107 | #endregion Case: Native BuiltIn Principal 108 | 109 | #region Case: Domain Specific BuiltIn Principal 110 | elseif ($importEntry.IsBuiltIn -eq 'True') 111 | { 112 | try { $domainObject = Resolve-DomainMapping -DomainSid ($importEntry.SID -as [System.Security.Principal.SecurityIdentifier]).AccountDomainSid.Value -DomainFqdn $importEntry.DomainFqdn -DomainName $importEntry.DomainName } 113 | catch { throw "Cannot resolve domain $($importEntry.DomainFqdn) for $($importEntry.Group) $($importEntry.Name)! $_" } 114 | 115 | $targetSID = '{0}-{1}' -f $domainObject.DomainSID, $importEntry.RID 116 | $adObject = Get-ADObject -Server $domainObject.DNSRoot -LDAPFilter "(&(objectClass=$($importEntry.Type))(objectSID=$($targetSID)))" 117 | if (-not $adObject) 118 | { 119 | Write-Warning "Failed to resolve AD identity: $($importEntry.Name) ($($targetSID))" 120 | continue 121 | } 122 | $targetName = $adObject.Name 123 | $script:identityMapping.Add(($importEntry | Select-Object *, $select_TargetName, $select_TargetDomain)) 124 | } 125 | #endregion Case: Domain Specific BuiltIn Principal 126 | 127 | #region Case: Custom Principal 128 | else 129 | { 130 | try { $domainObject = Resolve-DomainMapping -DomainSid ($importEntry.SID -as [System.Security.Principal.SecurityIdentifier]).AccountDomainSid.Value -DomainFqdn $importEntry.DomainFqdn -DomainName $importEntry.DomainName } 131 | catch { throw "Cannot resolve domain $($importEntry.DomainFqdn) for $($importEntry.Group) $($importEntry.Name)! $_" } 132 | 133 | $adObject = Get-ADObject -Server $domainObject.DNSRoot -LDAPFilter "(&(objectClass=$($importEntry.Type))(name=$($importEntry.Name)))" 134 | if (-not $adObject) 135 | { 136 | Write-Warning "Failed to resolve AD identity: $($importEntry.Name)" 137 | continue 138 | } 139 | $targetName = $adObject.Name 140 | $script:identityMapping.Add(($importEntry | Select-Object *, $select_TargetName, $select_TargetDomain)) 141 | } 142 | #endregion Case: Custom Principal 143 | } 144 | #endregion Case: Discovery 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /GPOTools/functions/Import-GptLink.ps1: -------------------------------------------------------------------------------- 1 | function Import-GptLink 2 | { 3 | <# 4 | .SYNOPSIS 5 | Imports GPO Links. 6 | 7 | .DESCRIPTION 8 | Imports GPO Links. 9 | Use this to restore the exported links in their original order (or as close to it as possible). 10 | 11 | .PARAMETER Path 12 | The path from which to pick up the import file. 13 | 14 | .PARAMETER Name 15 | Only restore links of matching GPOs 16 | 17 | .PARAMETER Domain 18 | The domain into which to import. 19 | 20 | .EXAMPLE 21 | PS C:\> Import-GptLink -Path '.' 22 | 23 | Import GPO Links based on the exported links stored in the current path. 24 | #> 25 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] 26 | [CmdletBinding()] 27 | param ( 28 | [Parameter(Mandatory = $true)] 29 | [string] 30 | $Path, 31 | 32 | [string[]] 33 | $Name = '*', 34 | 35 | [string] 36 | $Domain = $env:USERDNSDOMAIN 37 | ) 38 | 39 | begin 40 | { 41 | #region Utility Functions 42 | function Get-OU 43 | { 44 | <# 45 | .SYNOPSIS 46 | Retrieves an OU. Caches results. 47 | 48 | .DESCRIPTION 49 | Retrieves an OU. Caches results. 50 | Results are cached separately for each domain/server. 51 | 52 | .PARAMETER DistinguishedName 53 | The name of the OU to check. 54 | 55 | .PARAMETER Server 56 | The domain or server to check against. 57 | 58 | .EXAMPLE 59 | PS C:\> Get-OU -DistinguishedName $dn -Server $Domain 60 | 61 | Return the OU pointed at with $dn if it exists. 62 | #> 63 | [CmdletBinding()] 64 | param ( 65 | [Parameter(Mandatory = $true)] 66 | [string] 67 | $DistinguishedName, 68 | 69 | [Parameter(Mandatory = $true)] 70 | [string] 71 | $Server 72 | ) 73 | 74 | if (-not $script:targetOUs) { $script:targetOUs = @{ } } 75 | if (-not $script:targetOUs[$Server]) { $script:targetOUs[$Server] = @{ } } 76 | 77 | if ($script:targetOUs[$Server].ContainsKey($DistinguishedName)) 78 | { 79 | return $script:targetOUs[$Server][$DistinguishedName] 80 | } 81 | 82 | try 83 | { 84 | $paramGetADOrganizationalUnit = @{ 85 | Identity = $DistinguishedName 86 | Server = $Server 87 | Properties = 'gpLink' 88 | ErrorAction = 'Stop' 89 | } 90 | $script:targetOUs[$Server][$DistinguishedName] = Get-ADOrganizationalUnit @paramGetADOrganizationalUnit 91 | } 92 | catch { $script:targetOUs[$Server][$DistinguishedName] = $null } 93 | return $script:targetOUs[$Server][$DistinguishedName] 94 | } 95 | 96 | function Set-GPLinkSet 97 | { 98 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] 99 | [CmdletBinding()] 100 | param ( 101 | $LinkObject, 102 | 103 | $Domain, 104 | 105 | $AllGpos, 106 | 107 | $Server 108 | ) 109 | 110 | foreach ($linkItem in $LinkObject) 111 | { 112 | $linkItem.Index = [int]($linkItem.Index) 113 | $linkItem.TotalCount = [int]($linkItem.TotalCount) 114 | } 115 | $orgUnit = Get-OU -DistinguishedName $LinkObject[0].TargetOU -Server $Domain 116 | $insertIndex = 1 117 | foreach ($linkItem in ($LinkObject | Sort-Object Index)) 118 | { 119 | if ($orgUnit.LinkedGroupPolicyObjects -contains $linkItem.Policy.CleanedPath) 120 | { 121 | $insertIndex = $orgUnit.LinkedGroupPolicyObjects.IndexOf($linkItem.Policy.CleanedPath) + 1 122 | continue 123 | } 124 | 125 | $paramSetGPLink = @{ 126 | LinkEnabled = 'Yes' 127 | Guid = $linkItem.Policy.ID 128 | Order = $insertIndex 129 | Domain = $Domain 130 | Enforced = 'No' 131 | Target = $orgUnit 132 | Server = $Server 133 | ErrorAction = 'Stop' 134 | } 135 | if ($linkItem.State -eq "1") { $paramSetGPLink['LinkEnabled'] = 'No' } 136 | if ($linkItem.State -eq "2") { $paramSetGPLink['Enforced'] = 'Yes' } 137 | 138 | try 139 | { 140 | $null = New-GPLink @paramSetGPLink 141 | New-ImportResult -Action 'Importing Group Policy Links' -Step 'Applying Link' -Target $linkItem.GpoName -Data $linkItem -Success $true 142 | } 143 | catch 144 | { 145 | if ($_.Exception.InnerException.HResult -eq 0x800700B7) 146 | { 147 | New-ImportResult -Action 'Importing Group Policy Links' -Step 'Applying Link: Already Exists' -Target $linkItem.GpoName -Data $linkItem -Success $true -ErrorData $_ 148 | } 149 | else 150 | { 151 | New-ImportResult -Action 'Importing Group Policy Links' -Step 'Applying Link' -Target $linkItem.GpoName -Data $linkItem -Success $false -ErrorData $_ 152 | } 153 | } 154 | 155 | $insertIndex++ 156 | } 157 | } 158 | #endregion Utility Functions 159 | 160 | $PSDefaultParameterValues['New-ImportResult:Action'] = 'Importing Group Policy Links' 161 | $PSDefaultParameterValues['New-ImportResult:Success'] = $false 162 | 163 | $pathItem = Get-Item -Path $Path 164 | if ($pathItem.Extension -eq '.csv') { $resolvedPath = $pathItem.FullName } 165 | else { $resolvedPath = (Get-ChildItem -Path $pathItem.FullName -Filter 'gp_links_*.csv' | Select-Object -First 1).FullName } 166 | if (-not $resolvedPath) { throw "Could not find GPO Links file in $($pathItem.FullName)" } 167 | 168 | $domainObject = Get-ADDomain -Server $Domain 169 | $policyObjects = Get-GPO -All -Domain $Domain | Select-Object *, @{ 170 | Name = 'CleanedPath' 171 | Expression = { $_.Path -replace $_.ID, $_.ID } 172 | } 173 | $linkData = Import-Csv $resolvedPath | Where-Object { 174 | Test-Overlap -ReferenceObject $_.GpoName -DifferenceObject $Name -Operator Like 175 | } | Select-Object *, @{ 176 | Name = "Policy" 177 | Expression = { 178 | $linkItem = $_ 179 | $policyObjects | Where-Object DisplayName -EQ $linkItem.GpoName 180 | } 181 | }, @{ 182 | Name = "TargetOU" 183 | Expression = { 184 | '{0},{1}' -f ($_.OUDN -replace ',DC=\w+'), $domainObject.DistinguishedName 185 | } 186 | } 187 | } 188 | process 189 | { 190 | $groupedLinks = $linkData | Group-Object -Property GpoName 191 | $groupedLinks | Where-Object Name -NotIn $policyObjects.DisplayName | ForEach-Object { 192 | New-ImportResult -Step 'Checking GPO existence' -Target $_.Name -Data $_.Group -ErrorData "GPO $($_.Name) does not exist" 193 | } 194 | $linksPolicyExists = ($groupedLinks | Where-Object Name -In $policyObjects.DisplayName).Group 195 | $linksPolicyExists | Where-Object { -not (Get-OU -DistinguishedName $_.TargetOU -Server $Domain) } | ForEach-Object { 196 | New-ImportResult -Step 'Checking OU existence' -Target $_.GpoName -Data $_ -ErrorData "OU $($_.TargetOU) does not exist, cannot link $($_.GpoName)" 197 | } 198 | $linksToProcess = $linksPolicyExists | Where-Object { Get-OU -DistinguishedName $_.TargetOU -Server $Domain } 199 | 200 | $groupedToProcess = $linksToProcess | Group-Object -Property TargetOU 201 | foreach ($linkSet in $groupedToProcess) 202 | { 203 | Set-GPLinkSet -LinkObject $linkSet.Group -Domain $domainObject.DNSRoot -AllGpos $policyObjects -Server $domainObject.PDCEmulator 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /GPOTools/functions/Import-GptObject.ps1: -------------------------------------------------------------------------------- 1 | function Import-GptObject 2 | { 3 | <# 4 | .SYNOPSIS 5 | Import Group Policy Objects previously exported using Export-GptObject. 6 | 7 | .DESCRIPTION 8 | Import Group Policy Objects previously exported using Export-GptObject. 9 | 10 | .PARAMETER Path 11 | The path where the GPO export folders are located. 12 | Note: GPO export folders have a GUID as name. 13 | 14 | .PARAMETER Name 15 | Only import GPOs with a matching name. 16 | 17 | .PARAMETER Domain 18 | THe destination domain to import into. 19 | 20 | .EXAMPLE 21 | PS C:\> Import-GptObject -Path '.' 22 | 23 | Import all GPO objects exported into the current folder. 24 | #> 25 | [CmdletBinding()] 26 | param ( 27 | [Parameter(Mandatory = $true)] 28 | [string] 29 | $Path, 30 | 31 | [string[]] 32 | $Name = '*', 33 | 34 | [string] 35 | $Domain = $env:USERDNSDOMAIN 36 | ) 37 | 38 | begin 39 | { 40 | $pdcEmulator = (Get-ADDomain -Server $Domain).PDCEmulator 41 | if (-not (Test-Path $Path)) 42 | { 43 | New-ImportResult -Action 'Importing Policy Objects' -Step 'Validating import path' -Target $Path -Success $false 44 | throw "Import path not found: $Path" 45 | } 46 | if ((Get-Item -Path $Path).Extension -eq '.csv') { $gpoFile = Get-Item -Path $Path } 47 | elseif (Test-Path -Path (Join-Path -Path $Path -ChildPath 'gp_object_*.csv')) { $gpoFile = Get-Item (Join-Path -Path $Path -ChildPath 'gp_object_*.csv') } 48 | elseif (Test-Path -Path (Join-Path -Path (Join-Path -Path $Path -ChildPath 'GPO') -ChildPath 'gp_object_*.csv')) { $gpoFile = Get-Item (Join-Path -Path (Join-Path -Path $Path -ChildPath 'GPO') -ChildPath 'gp_object_*.csv') } 49 | else 50 | { 51 | New-ImportResult -Action 'Importing Policy Objects' -Step 'Validating import path' -Target $Path -Success $false 52 | throw "Could not find GPO backup index under: $Path" 53 | } 54 | $gpoData = Import-Csv -Path $gpoFile.FullName 55 | 56 | try { $migrationTablePath = New-MigrationTable -Path $gpoFile.DirectoryName -BackupPath $gpoFile.DirectoryName -Domain $Domain -ErrorAction Stop } 57 | catch 58 | { 59 | New-ImportResult -Action 'Importing Policy Objects' -Step 'Creating Migration Table' -Target $Path -Success $false -ErrorData $_ 60 | throw 61 | } 62 | } 63 | process 64 | { 65 | foreach ($gpoEntry in $gpoData) 66 | { 67 | if (-not (Test-Overlap -ReferenceObject $gpoEntry.DisplayName -DifferenceObject $Name -Operator Like)) 68 | { 69 | continue 70 | } 71 | 72 | $paramImportGPO = @{ 73 | Domain = $Domain 74 | Server = $pdcEmulator 75 | BackupGpoName = $gpoEntry.DisplayName 76 | TargetName = $gpoEntry.DisplayName 77 | Path = $gpoFile.DirectoryName 78 | MigrationTable = $migrationTablePath 79 | CreateIfNeeded = $true 80 | ErrorAction = 'Stop' 81 | } 82 | try 83 | { 84 | Write-Verbose "Importing Policy object: $($gpoEntry.DisplayName)" 85 | $importedGPO = Import-GPO @paramImportGPO 86 | if ($gpoEntry.WmiFilter) 87 | { 88 | $wmiFilter = Get-ADObject -SearchBase "CN=SOM,CN=WMIPolicy,$((Get-ADDomain -Server $pdcEmulator).SystemsContainer)" -LDAPFilter "(&(objectClass=msWMI-Som)(msWMI-Name=$($gpoEntry.WmiFilter)))" 89 | Set-ADObject -Identity $importedGPO.Path -Replace @{ gPCWQLFilter = "[$Domain;$($wmiFilter.Name);0]" } -Server $pdcEmulator 90 | } 91 | # Mapped network drives are not correctly covered by Migration Tables 92 | Update-NetworkDrive -GpoName $gpoEntry.DisplayName -Domain $Domain 93 | New-ImportResult -Action 'Importing Policy Objects' -Step 'Import Object' -Target $gpoEntry -Success $true -Data $gpoEntry, $migrationTablePath 94 | } 95 | catch 96 | { 97 | New-ImportResult -Action 'Importing Policy Objects' -Step 'Import Object' -Target $gpoEntry -Success $false -Data $gpoEntry, $migrationTablePath -ErrorData $_ 98 | Write-Error $_ 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /GPOTools/functions/Import-GptPermission.ps1: -------------------------------------------------------------------------------- 1 | function Import-GptPermission 2 | { 3 | <# 4 | .SYNOPSIS 5 | Import permissions to GPOs. 6 | 7 | .DESCRIPTION 8 | Import permissions to GPOs. 9 | This tries to restore the same permissions that existed on the GPOs before the export. 10 | Notes: 11 | - It is highly recommended to perform this before executing Import-GptLink. 12 | - Executing this requires the identities to have been imported (Import-GptIdentity) 13 | 14 | .PARAMETER Path 15 | The path where the permission export file is stored. 16 | 17 | .PARAMETER Name 18 | Only restore permissions for GPOs with a matching name. 19 | 20 | .PARAMETER GpoObject 21 | Select the GPOs to restore permissions to by specifying their full object. 22 | 23 | .PARAMETER ExcludeInherited 24 | Do not import permissions that were inherited permissions on the source GPO 25 | 26 | .PARAMETER Domain 27 | The domain to restore the GPO permissions to. 28 | 29 | .EXAMPLE 30 | PS C:\> Import-GptPermission -Path '.' 31 | 32 | Import GPO permissions from the current path. 33 | #> 34 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] 35 | [CmdletBinding()] 36 | param ( 37 | [Parameter(Mandatory = $true)] 38 | [ValidateScript({ Test-Path -Path $_ })] 39 | [string] 40 | $Path, 41 | 42 | [string[]] 43 | $Name = '*', 44 | 45 | [Parameter(ValueFromPipeline = $true)] 46 | $GpoObject, 47 | 48 | [switch] 49 | $ExcludeInherited, 50 | 51 | [string] 52 | $Domain = $env:USERDNSDOMAIN 53 | ) 54 | 55 | begin 56 | { 57 | #region Utility Functions 58 | function Update-GpoPermission 59 | { 60 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] 61 | [CmdletBinding()] 62 | param ( 63 | $ADObject, 64 | 65 | $Permission, 66 | 67 | $GpoObject, 68 | 69 | $DomainObject 70 | ) 71 | 72 | try 73 | { 74 | $accessRule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList @( 75 | (ConvertFrom-ImportedIdentity -Permission $Permission -DomainObject $DomainObject), 76 | $Permission.ActiveDirectoryRights, 77 | $Permission.AccessControlType, 78 | $Permission.ObjectType, 79 | $Permission.InheritanceType, 80 | $Permission.InheritedObjectType 81 | ) 82 | } 83 | catch 84 | { 85 | New-ImportResult -Action 'Update Gpo Permission' -Step 'Resolving Identity' -Target $Permission.GpoName -Success $false -Data $Permission -ErrorData $_ 86 | return 87 | } 88 | 89 | $matchingRule = $null 90 | $matchingRule = $ADObject.ntSecurityDescriptor.Access | Where-Object { 91 | $accessRule.IdentityReference -eq $_.IdentityReference -and 92 | $accessRule.ActiveDirectoryRights -eq $_.ActiveDirectoryRights -and 93 | $accessRule.AccessControlType -eq $_.AccessControlType -and 94 | $accessRule.ObjectType -eq $_.ObjectType -and 95 | $accessRule.InheritanceType -eq $_.InheritanceType -and 96 | $accessRule.InheritedObjectType -eq $_.InheritedObjectType 97 | } 98 | 99 | if ($matchingRule) 100 | { 101 | New-ImportResult -Action 'Update Gpo Permission' -Step 'Skipped, already exists' -Target $Permission.GpoName -Success $true -Data $Permission, $accessRule 102 | return 103 | } 104 | 105 | #region Set AD Permissions 106 | try 107 | { 108 | Write-Verbose "Updating ACL on GPO $($ADObject.DistinguishedName)" 109 | $acl = Get-Acl -Path "AD:\$($ADObject.DistinguishedName)" -ErrorAction Stop 110 | $acl.AddAccessRule($accessRule) 111 | $acl | Set-Acl -Path "AD:\$($ADObject.DistinguishedName)" -ErrorAction Stop 112 | } 113 | catch 114 | { 115 | New-ImportResult -Action 'Update Gpo Permission' -Step 'Apply AD Permission' -Target $Permission.GpoName -Success $false -Data $Permission, $accessRule -ErrorData $_ 116 | continue 117 | } 118 | #endregion Set AD Permissions 119 | 120 | #region Set File Permissions 121 | if (-not (Test-Path $ADObject.gPCFileSysPath)) 122 | { 123 | New-ImportResult -Action 'Update Gpo Permission' -Step 'Apply File Permission' -Target $Permission.GpoName -Success $false -Data $Permission, $accessRule -ErrorData "Path not found" 124 | continue 125 | } 126 | try 127 | { 128 | $rights = 'Read' 129 | if ($accessRule.ActiveDirectoryRights -eq 983295) { $rights = 'FullControl' } 130 | $fileRule = New-Object System.Security.AccessControl.FileSystemAccessRule -ArgumentList @( 131 | $accessRule.IdentityReference 132 | $rights 133 | $accessRule.AccessControlType 134 | ) 135 | 136 | 137 | $acl = Get-Acl -Path $ADObject.gPCFileSysPath -ErrorAction Stop 138 | $acl.AddAccessRule($fileRule) 139 | $acl | Set-Acl -Path $ADObject.gPCFileSysPath -ErrorAction Stop 140 | } 141 | catch 142 | { 143 | New-ImportResult -Action 'Update Gpo Permission' -Step 'Apply File Permission' -Target $Permission.GpoName -Success $false -Data $Permission, $accessRule -ErrorData $_ 144 | continue 145 | } 146 | #endregion Set File Permissions 147 | 148 | New-ImportResult -Action 'Update Gpo Permission' -Step Success -Target $Permission.GpoName -Success $true -Data $Permission, $accessRule 149 | } 150 | #endregion Utility Functions 151 | 152 | $pathItem = Get-Item -Path $Path 153 | if ($pathItem.Extension -eq '.csv') { $resolvedPath = $pathItem.FullName } 154 | else { $resolvedPath = (Get-ChildItem -Path $pathItem.FullName -Filter 'gp_permissions_*.csv' | Select-Object -First 1).FullName } 155 | if (-not $resolvedPath) { throw "Could not find permissions file in $($pathItem.FullName)" } 156 | 157 | if (-not $script:identityMapping) 158 | { 159 | throw 'Could not find imported identities to match. Please run Import-GptIdentity first!' 160 | } 161 | 162 | $domainObject = Get-ADDomain -Server $Domain 163 | $allPermissionData = Import-Csv -Path $resolvedPath 164 | } 165 | process 166 | { 167 | $gpoObjects = $GpoObject 168 | if (-not $GpoObject) 169 | { 170 | $gpoObjects = Get-GPO -All -Domain $Domain 171 | } 172 | 173 | foreach ($gpoItem in $gpoObjects) 174 | { 175 | if (-not (Test-Overlap -ReferenceObject $gpoItem.DisplayName -DifferenceObject $Name -Operator Like)) 176 | { 177 | continue 178 | } 179 | $adObject = Get-ADObject -Identity $gpoItem.Path -Server $gpoItem.DomainName -Properties ntSecurityDescriptor, gPCFileSysPath 180 | 181 | foreach ($permission in $allPermissionData) 182 | { 183 | # Skip items that do not apply 184 | if ($permission.GpoName -ne $gpoItem.DisplayName) { continue } 185 | if ($ExcludeInherited -and $permission.IsInherited -eq "True") { continue } 186 | 187 | Update-GpoPermission -ADObject $adObject -Permission $permission -GpoObject $gpoItem -DomainObject $domainObject 188 | } 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /GPOTools/functions/Import-GptWmiFilter.ps1: -------------------------------------------------------------------------------- 1 | function Import-GptWmiFilter 2 | { 3 | <# 4 | .SYNOPSIS 5 | Imports WMI filters. 6 | 7 | .DESCRIPTION 8 | Imports WMI filters stored to file using Export-GptWmiFilter. 9 | Note: This should be performed before using Import-GptPolicy. 10 | 11 | .PARAMETER Path 12 | The path from which to import the WmiFilters 13 | 14 | .PARAMETER Domain 15 | The domain into which to import the WmiFilters 16 | 17 | .EXAMPLE 18 | PS C:\> Import-GptWmiFilter -Path '.' 19 | 20 | Import WMI Filters from the current path. 21 | #> 22 | [CmdletBinding()] 23 | param ( 24 | [ValidateScript({ Test-Path -Path $_ })] 25 | [Parameter(Mandatory = $true)] 26 | [string] 27 | $Path, 28 | 29 | [string] 30 | $Domain = $env:USERDNSDOMAIN 31 | ) 32 | 33 | begin 34 | { 35 | $pathItem = Get-Item -Path $Path 36 | if ($pathItem.Extension -eq '.csv') { $resolvedPath = $pathItem.FullName } 37 | else { $resolvedPath = (Get-ChildItem -Path $pathItem.FullName -Filter 'gp_wmifilters_*.csv' | Select-Object -First 1).FullName } 38 | if (-not $resolvedPath) { throw "Could not find WMI Filters file in $($pathItem.FullName)" } 39 | 40 | $allWmiFilterEntries = Import-Csv -Path $resolvedPath 41 | $namingContext = (Get-ADRootDSE -Server $Domain).DefaultNamingContext 42 | $pdcEmulator = (Get-ADDomain -Server $Domain).PDCEmulator 43 | } 44 | process 45 | { 46 | foreach ($wmiFilter in $allWmiFilterEntries) 47 | { 48 | #region Update Existing 49 | if ($adObject = Get-ADObject -Server $pdcEmulator -LDAPFilter "(&(objectClass=msWMI-Som)(msWMI-Name=$($wmiFilter.Name)))") 50 | { 51 | $adObject | Set-ADObject -Server $pdcEmulator -Replace @{ 52 | 'msWMI-Author' = $wmiFilter.Author 53 | 'msWMI-Parm1' = $wmiFilter.Description 54 | 'msWMI-Parm2' = $wmiFilter.Filter 55 | } 56 | } 57 | #endregion Update Existing 58 | 59 | #region Create New 60 | else 61 | { 62 | $wmiGuid = "{$([System.Guid]::NewGuid())}" 63 | $creationDate = (Get-Date).ToUniversalTime().ToString("yyyyMMddhhmmss.ffffff-000") 64 | 65 | $attributes = @{ 66 | "showInAdvancedViewOnly" = "TRUE" 67 | "msWMI-Name" = $wmiFilter.Name 68 | "msWMI-Parm1" = $wmiFilter.Description 69 | "msWMI-Parm2" = $wmiFilter.Filter 70 | "msWMI-Author" = $wmiFilter.Author 71 | "msWMI-ID" = $wmiGuid 72 | "instanceType" = 4 73 | "distinguishedname" = "CN=$wmiGuid,CN=SOM,CN=WMIPolicy,CN=System,$namingContext" 74 | "msWMI-ChangeDate" = $creationDate 75 | "msWMI-CreationDate" = $creationDate 76 | } 77 | 78 | $paramNewADObject = @{ 79 | OtherAttributes = $attributes 80 | Name = $wmiGuid 81 | Type = "msWMI-Som" 82 | Path = "CN=SOM,CN=WMIPolicy,CN=System,$namingContext" 83 | Server = $pdcEmulator 84 | } 85 | 86 | $null = New-ADObject @paramNewADObject 87 | } 88 | #endregion Create New 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /GPOTools/functions/Register-GptDomainMapping.ps1: -------------------------------------------------------------------------------- 1 | function Register-GptDomainMapping { 2 | <# 3 | .SYNOPSIS 4 | Maps source domain names to the associated target domain. 5 | 6 | .DESCRIPTION 7 | Maps source domain names to the associated target domain. 8 | This is used to map source identities to the correct destination domains. 9 | This data is used during import/restore only! 10 | 11 | .PARAMETER SourceName 12 | Netbios name of the source domain. 13 | Last resort for source identity domain translation. 14 | 15 | .PARAMETER SourceFQDN 16 | FQDN of the source domain. 17 | 18 | .PARAMETER SourceSID 19 | SID of the source domain. 20 | Primary tool for source identity domain translation. 21 | 22 | .PARAMETER Destination 23 | The destination domain. 24 | Either offer an active directory domain object (Returned by Get-ADDOmain) or a name that will be looked up. 25 | 26 | .PARAMETER Server 27 | Server to use for looking up the destination domain data. 28 | Used only when the Destination parameter waas set to string value (such as the fqdn of the domain). 29 | 30 | .EXAMPLE 31 | PS C:\> Register-GptDomainMapping -SourceName corp -SourceFQDN corp.contoso.com -SourceSID $sid -Destination $domain 32 | 33 | Registers name mappings, pointing corp.contoso.com to the destination domain stored in $domain. 34 | #> 35 | [CmdletBinding()] 36 | param ( 37 | [string] 38 | $SourceName, 39 | 40 | [string] 41 | $SourceFQDN, 42 | 43 | [string] 44 | $SourceSID, 45 | 46 | $Destination, 47 | [string] 48 | 49 | $Server 50 | ) 51 | 52 | begin 53 | { 54 | if (-not $script:domainMapping) { 55 | $script:domainMapping = @{ 56 | Name = @{ } 57 | FQDN = @{ } 58 | SID = @{ } 59 | } 60 | } 61 | # Do not check for actual type, in order to allow users to fake/mock up a custom object 62 | if ($Destination.PSObject.TypeNames -contains 'Microsoft.ActiveDirectory.Management.ADDomain') { 63 | $domainObject = $Destination 64 | } 65 | else { 66 | $params = @{ 67 | Identity = $Destination 68 | ErrorAction = 'Stop' 69 | } 70 | if ($Server) { $params['Server'] = $Server } 71 | try { $domainObject = Get-ADDomain @params } 72 | catch { 73 | Write-Warning "Failed to resolve destination domain: $Destination : $_" 74 | throw 75 | } 76 | } 77 | } 78 | process 79 | { 80 | if ($SourceName) { 81 | $script:domainMapping.Name[$SourceName] = $domainObject 82 | } 83 | if ($SourceFQDN) { 84 | $script:domainMapping.FQDN[$SourceFQDN] = $domainObject 85 | } 86 | if ($SourceSID) { 87 | $script:domainMapping.SID[$SourceSID] = $domainObject 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /GPOTools/functions/Restore-GptPolicy.ps1: -------------------------------------------------------------------------------- 1 | function Restore-GptPolicy 2 | { 3 | <# 4 | .SYNOPSIS 5 | Performs a full restore of GPOs exported with Backup-GptPolicy. 6 | 7 | .DESCRIPTION 8 | Performs a full restore of GPOs exported with Backup-GptPolicy. 9 | This includes executing all the relevant import commands in the optimal order. 10 | 11 | .PARAMETER Path 12 | The root path into which the backup was exported. 13 | 14 | .PARAMETER Name 15 | Only restore GPOs with matching name. 16 | 17 | .PARAMETER Domain 18 | The domain into which to restore the policy objects. 19 | 20 | .PARAMETER IdentityMapping 21 | A hashtable mapping source identities to destination identities. 22 | Use this to map groups that do not share the same name between source and destination. 23 | 24 | .EXAMPLE 25 | PS C:\> Restore-GptPolicy -Path '.' 26 | 27 | Perform a full restore/import of the backup written to the current folder. 28 | #> 29 | [CmdletBinding()] 30 | param ( 31 | [Parameter(Mandatory = $true)] 32 | [string] 33 | $Path, 34 | 35 | [string[]] 36 | $Name = '*', 37 | 38 | [string] 39 | $Domain = $env:USERDNSDOMAIN, 40 | 41 | [hashtable] 42 | $IdentityMapping = @{} 43 | ) 44 | 45 | begin 46 | { 47 | $common = @{ 48 | Path = $Path 49 | Domain = $Domain 50 | } 51 | 52 | Write-Verbose "Importing Domain Data" 53 | Import-GptDomainData @common 54 | } 55 | process 56 | { 57 | Write-Verbose "Importing Identities" 58 | Import-GptIdentity @common -Mapping $IdentityMapping 59 | Write-Verbose "Importing WMI Filters" 60 | Import-GptWmiFilter @common 61 | Write-Verbose "Importing Objects" 62 | Import-GptObject @common -Name $Name 63 | Write-Verbose "Importing Permissions" 64 | Import-GptPermission @common -Name $Name 65 | Write-Verbose "Importing GPO Links" 66 | Import-GptLink @common -Name $Name 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /GPOTools/functions/readme.md: -------------------------------------------------------------------------------- 1 | # Functions 2 | 3 | This is the folder where the functions go. 4 | 5 | Depending on the complexity of the module, it is recommended to subdivide them into subfolders. 6 | 7 | The module will pick up all .ps1 files recursively -------------------------------------------------------------------------------- /GPOTools/internal/configurations/configuration.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | This is an example configuration file 3 | 4 | By default, it is enough to have a single one of them, 5 | however if you have enough configuration settings to justify having multiple copies of it, 6 | feel totally free to split them into multiple files. 7 | #> 8 | 9 | <# 10 | # Example Configuration 11 | Set-PSFConfig -Module 'GPOTools' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" 12 | #> 13 | 14 | Set-PSFConfig -Module 'GPOTools' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." 15 | Set-PSFConfig -Module 'GPOTools' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." -------------------------------------------------------------------------------- /GPOTools/internal/configurations/readme.md: -------------------------------------------------------------------------------- 1 | # Configurations 2 | 3 | Through the `PSFramework` you have a simple method that allows you to ... 4 | 5 | - Publish settings 6 | - With onboard documentation 7 | - Input validation 8 | - Scripts that run on change of settings 9 | - That can be discovered and updated by the user 10 | - That can be administrated by policy & DSC 11 | 12 | The configuration system is a bit too complex to describe in a help file, you can however visit us at http://psframework.org for detailed guidance. 13 | 14 | An example can be seen in the attached ps1 file -------------------------------------------------------------------------------- /GPOTools/internal/functions/ConvertFrom-ImportedIdentity.ps1: -------------------------------------------------------------------------------- 1 | function ConvertFrom-ImportedIdentity 2 | { 3 | <# 4 | .SYNOPSIS 5 | Converts an imported identity into a security principal. 6 | 7 | .DESCRIPTION 8 | Converts an imported identity into a security principal. 9 | This is used for granting permissions. 10 | 11 | .PARAMETER Permission 12 | The permission object containing the source principal. 13 | 14 | .PARAMETER DomainObject 15 | An object representing the destination domain (as returned by Get-ADDomain) 16 | 17 | .EXAMPLE 18 | PS C:\> ConvertFrom-ImportedIdentity -Permission $permission -DomainObject $domainObject 19 | 20 | Resolves the source identity into a destination security principal. 21 | #> 22 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] 23 | [OutputType([System.Security.Principal.IdentityReference])] 24 | [CmdletBinding()] 25 | param ( 26 | [Parameter(Mandatory = $true)] 27 | $Permission, 28 | 29 | [Parameter(Mandatory = $true)] 30 | $DomainObject 31 | ) 32 | 33 | process 34 | { 35 | switch ($Permission.PrincipalType) 36 | { 37 | 'Local BuiltIn' { return [System.Security.Principal.SecurityIdentifier]$Permission.SID } 38 | 'foreignSecurityPrincipal' { return [System.Security.Principal.SecurityIdentifier]$Permission.SID } 39 | 'group' 40 | { 41 | #TODO: Implement Domain Resolution 42 | try { $domainObject = Resolve-DomainMapping -DomainSid ($Permission.SID -as [System.Security.Principal.SecurityIdentifier]).AccountDomainSid.Value -DomainFqdn $Permission.DomainFqdn -DomainName $Permission.DomainName } 43 | catch { throw "Cannot resolve domain $($Permission.DomainFqdn) for $($Permission.Group) $($Permission.SID)! $_" } 44 | 45 | if ($Permission.IsBuiltIn -like 'true') 46 | { 47 | return [System.Security.Principal.SecurityIdentifier]('{0}-{1}' -f $DomainObject.DomainSID, $Permission.RID) 48 | } 49 | else 50 | { 51 | $identity = $script:identityMapping | Where-Object SID -EQ $Permission.SID 52 | if (-not $identity) { throw "Cannot resolve $($Permission.IdentityReference) ($($Permission.SID))" } 53 | return [System.Security.Principal.NTAccount]('{0}\{1}' -f $DomainObject.NetBIOSName, $identity.Target) 54 | } 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /GPOTools/internal/functions/ConvertTo-DnsDomainName.ps1: -------------------------------------------------------------------------------- 1 | function ConvertTo-DnsDomainName 2 | { 3 | <# 4 | .SYNOPSIS 5 | Converts a distinguished name in the DNS domain name. 6 | 7 | .DESCRIPTION 8 | This extracts the domain portion of a distinguished name and processes it as dns name. 9 | 10 | .PARAMETER DistinguishedName 11 | The name to parse / convert. 12 | 13 | .EXAMPLE 14 | PS C:\> Get-ADDomain | ConvertTo-DnsDomainName 15 | 16 | Returns the dns name of the current domain. 17 | #> 18 | [CmdletBinding()] 19 | param ( 20 | [Parameter(ValueFromPipeline = $true, Mandatory = $true)] 21 | [Alias('Name')] 22 | [string[]] 23 | $DistinguishedName 24 | ) 25 | 26 | process 27 | { 28 | foreach ($distName in $DistinguishedName) 29 | { 30 | ($distName -split "," | Where-Object { $_ -like "DC=*" } | ForEach-Object { 31 | $_ -replace '^DC=' 32 | }) -join "." 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /GPOTools/internal/functions/Get-DomainData.ps1: -------------------------------------------------------------------------------- 1 | function Get-DomainData 2 | { 3 | <# 4 | .SYNOPSIS 5 | Retrieves common domain data, while caching results. 6 | 7 | .DESCRIPTION 8 | Retrieves common domain data, while caching results. 9 | Reduces overhead of looking up the same object again and again. 10 | 11 | .PARAMETER Domain 12 | The domain to retrieve data for. 13 | 14 | .EXAMPLE 15 | PS C:\> Get-DomainData -Domain Contoso.com 16 | 17 | Returns domain data for the domain contoso.com 18 | #> 19 | [CmdletBinding()] 20 | param ( 21 | [Parameter(Mandatory = $true)] 22 | [string] 23 | $Domain 24 | ) 25 | 26 | begin 27 | { 28 | if (-not $script:domainData) 29 | { 30 | $script:domainData = @{ } 31 | 32 | #region Pre-Seed information for all domains in forest 33 | $forestObject = Get-ADForest 34 | $domains = $forestObject.Domains | Foreach-Object { Get-ADDomain -Server $_ -Identity $_ } | ForEach-Object { 35 | [PSCustomObject]@{ 36 | DistinguishedName = $_.DistinguishedName 37 | Name = $_.Name 38 | SID = $_.DomainSID 39 | Fqdn = $_.DNSRoot 40 | ADObject = $_ 41 | } 42 | } 43 | foreach ($domainObject in $domains) 44 | { 45 | $script:domainData["$($domainObject.SID)"] = $domainObject 46 | $script:domainData[$domainObject.Fqdn] = $domainObject 47 | $script:domainData[$domainObject.DistinguishedName] = $domainObject 48 | } 49 | #endregion Pre-Seed information for all domains in forest 50 | } 51 | } 52 | process 53 | { 54 | if ($script:domainData[$Domain]) 55 | { 56 | return $script:domainData[$Domain] 57 | } 58 | 59 | #region Collect information for unknown domain 60 | if ($Domain -as [System.Security.Principal.SecurityIdentifier]) { $domainObject = Get-ADDomain -Identity $Domain -ErrorAction Stop } 61 | else { $domainObject = Get-ADDomain -Server $Domain -ErrorAction Stop } 62 | 63 | $domainObjectProcessed = [PSCustomObject]@{ 64 | DistinguishedName = $domainObject.DistinguishedName 65 | Name = $domainObject.Name 66 | SID = $domainObject.DomainSID 67 | Fqdn = $domainObject.DNSRoot 68 | ADObject = $domainObject 69 | } 70 | $script:domainData["$($domainObjectProcessed.SID)"] = $domainObjectProcessed 71 | $script:domainData[$domainObjectProcessed.Fqdn] = $domainObjectProcessed 72 | $script:domainData[$domainObjectProcessed.DistinguishedName] = $domainObjectProcessed 73 | $script:domainData[$Domain] = $domainObjectProcessed 74 | $script:domainData[$Domain] 75 | #endregion Collect information for unknown domain 76 | } 77 | } -------------------------------------------------------------------------------- /GPOTools/internal/functions/New-ImportResult.ps1: -------------------------------------------------------------------------------- 1 | function New-ImportResult 2 | { 3 | <# 4 | .SYNOPSIS 5 | Create unified import result objects. 6 | 7 | .DESCRIPTION 8 | Create unified import result objects. 9 | 10 | .PARAMETER Action 11 | The action taken. 12 | 13 | .PARAMETER Step 14 | The current step of the action. 15 | 16 | .PARAMETER Target 17 | The target of the step. 18 | 19 | .PARAMETER Success 20 | Whether the action was a success. 21 | 22 | .PARAMETER Data 23 | Any data to add to the report 24 | 25 | .PARAMETER ErrorData 26 | Any error data to add to the report 27 | 28 | .EXAMPLE 29 | PS C:\> New-ImportResult -Action 'Importing Policy Objects' -Step 'Import Object' -Target $gpoEntry -Success $true -Data $gpoEntry, $migrationTablePath 30 | 31 | Creates a new object representing a successful GPO import. 32 | #> 33 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] 34 | [CmdletBinding()] 35 | param ( 36 | [Parameter(Mandatory = $true)] 37 | [string] 38 | $Action, 39 | 40 | [Parameter(Mandatory = $true)] 41 | [string] 42 | $Step, 43 | 44 | $Target, 45 | 46 | [Parameter(Mandatory = $true)] 47 | [bool] 48 | $Success, 49 | 50 | $Data, 51 | 52 | $ErrorData 53 | ) 54 | 55 | [pscustomobject]@{ 56 | PSTypeName = 'GPOTools.ImportResult' 57 | Action = $Action 58 | Step = $Step 59 | Target = $Target 60 | Success = $Success 61 | Data = $Data 62 | Error = $ErrorData 63 | } 64 | } -------------------------------------------------------------------------------- /GPOTools/internal/functions/New-MigrationTable.ps1: -------------------------------------------------------------------------------- 1 | function New-MigrationTable 2 | { 3 | <# 4 | .SYNOPSIS 5 | Creates a new migration table used for GPO imports. 6 | 7 | .DESCRIPTION 8 | Creates a new migration table used for GPO imports. 9 | In this table, all source identities get matched to fitting destination identities. 10 | This ensures, that all identity references within GPOs remain intact. 11 | 12 | .PARAMETER Path 13 | The path where to spawn the migration table. 14 | Specify a folder, the file will be named '.migtable' 15 | 16 | .PARAMETER BackupPath 17 | The path where the GPO backups are stored. 18 | 19 | .PARAMETER Domain 20 | The domain the backup will be restored to. 21 | Defaults to the current user's domain. 22 | 23 | .EXAMPLE 24 | PS C:\> New-MigrationTable -Path '.' -BackupPath '.' 25 | 26 | Creates a migration table in the current path and looks in the current path for backup folders. 27 | #> 28 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] 29 | [CmdletBinding()] 30 | param ( 31 | [Parameter(Mandatory = $true)] 32 | [string] 33 | $Path, 34 | 35 | [Parameter(Mandatory = $true)] 36 | [string] 37 | $BackupPath, 38 | 39 | [string] 40 | $Domain = $env:USERDNSDOMAIN 41 | ) 42 | 43 | begin 44 | { 45 | $resolvedPath = (Resolve-Path $Path).ProviderPath 46 | $resolvedBackupPath = (Resolve-Path $BackupPath).ProviderPath 47 | $writePath = Join-Path -Path $resolvedPath -ChildPath "$Domain.migtable" 48 | 49 | #region Resolving source and destination Domain Names 50 | $domainData = Get-DomainData -Domain $Domain 51 | $destDomainDNS = $domainData.Fqdn 52 | $destDomainNetBios = $domainData.ADObject.NetBIOSName 53 | 54 | if ($script:sourceDomainData) 55 | { 56 | $sourceDomainDNS = $script:sourceDomainData.DomainDNSName 57 | $sourceDomainNetBios = $script:sourceDomainData.NetBIOSName 58 | } 59 | elseif ($script:identityMapping.Count -gt 0) 60 | { 61 | $sourceDomainDNS = $script:identityMapping[0].DomainFqdn 62 | $sourceDomainNetBios = $script:identityMapping[0].DomainName 63 | } 64 | else 65 | { 66 | throw "Unable to determine source domain. Run Import-GptDomainData or Import-GptIdentity first!" 67 | } 68 | #endregion Resolving source and destination Domain Names 69 | 70 | #region Preparing imported identities 71 | $explicitIdentityMappings = foreach ($identity in $script:identityMapping) 72 | { 73 | if (($identity.IsBuiltIn -eq 'True') -and ($identity.SID -like "*-32-*")) 74 | { 75 | [PSCustomObject]@{ 76 | Source = $identity.Name 77 | Target = $identity.Target 78 | } 79 | } 80 | else 81 | { 82 | [PSCustomObject]@{ 83 | Source = ('{0}\{1}' -f $identity.DomainName, $identity.Name) 84 | Target = ('{0}\{1}' -f $identity.TargetDomain.Name, $identity.Target) 85 | } 86 | [PSCustomObject]@{ 87 | Source = ('{0}@{1}' -f $identity.Name, $identity.DomainFqdn) 88 | Target = ('{0}@{1}' -f $identity.Target, $identity.TargetDomain.DNSRoot) 89 | } 90 | } 91 | } 92 | #endregion Preparing imported identities 93 | } 94 | process 95 | { 96 | #region Preparing basic migration table 97 | $groupPolicyManager = New-Object -ComObject GPMgmt.GPM 98 | $migrationTable = $groupPolicyManager.CreateMigrationTable() 99 | $constants = $groupPolicyManager.getConstants() 100 | $backupDirectory = $groupPolicyManager.GetBackupDir($resolvedBackupPath) 101 | $backupList = $backupDirectory.SearchBackups($groupPolicyManager.CreateSearchCriteria()) 102 | 103 | foreach ($policyBackup in $backupList) 104 | { 105 | $migrationTable.Add(0, $policyBackup) 106 | $migrationTable.Add($constants.ProcessSecurity, $policyBackup) 107 | } 108 | #endregion Preparing basic migration table 109 | 110 | #region Applying identity and UNC mappings 111 | foreach ($entry in $migrationTable.GetEntries()) 112 | { 113 | switch ($entry.EntryType) 114 | { 115 | $constants.EntryTypeUNCPath 116 | { 117 | if ($entry.Source -like "\\$sourceDomainDNS\*") 118 | { 119 | $null = $migrationTable.UpdateDestination($entry.Source, $entry.Source.Replace("\\$sourceDomainDNS\", "\\$destDomainDNS\")) 120 | } 121 | if ($entry.Source -like "\\$sourceDomainNetBios\*") 122 | { 123 | $null = $migrationTable.UpdateDestination($entry.Source, $entry.Source.Replace("\\$sourceDomainNetBios\", "\\$destDomainNetBios\")) 124 | } 125 | } 126 | 127 | { $constants.EntryTypeUser, $constants.EntryTypeGlobalGroup, $constants.EntryTypeUniversalGroup, $constants.EntryTypeUnknown -contains $_ } { 128 | if ($mapping = $explicitIdentityMappings | Where-Object Source -EQ $entry.Source) 129 | { 130 | $null = $migrationTable.UpdateDestination($entry.Source, $mapping.Target) 131 | } 132 | } 133 | } 134 | } 135 | 136 | # Additionally scan backup for share mappings, as those won't be found by default 137 | foreach ($gpoFolder in (Get-ChildItem -Path $resolvedBackupPath -Directory | Where-Object Name -Match '^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$')) 138 | { 139 | $driveXmlPath = Join-Path -Path $gpoFolder.FullName -ChildPath 'DomainSysvol\GPO\User\Preferences\Drives\Drives.xml' 140 | if (-not (Test-Path -Path $driveXmlPath)) { continue } 141 | 142 | try { $driveXmlData = [xml](Get-Content -Path $driveXmlPath) } 143 | catch { continue } 144 | 145 | foreach ($driveSet in $driveXmlData.Drives.Drive) 146 | { 147 | if ($driveSet.Properties.Path -like "\\$sourceDomainDNS\*") 148 | { 149 | $null = $migrationTable.AddEntry($driveSet.Properties.Path, $constants.EntryTypeUNCPath, $driveSet.Properties.Path.Replace("\\$sourceDomainDNS\", "\\$destDomainDNS\")) 150 | } 151 | if ($driveSet.Properties.Path -like "\\$sourceDomainNetBios\*") 152 | { 153 | $null = $migrationTable.AddEntry($driveSet.Properties.Path, $constants.EntryTypeUNCPath, $driveSet.Properties.Path.Replace("\\$sourceDomainNetBios\", "\\$destDomainNetBios\")) 154 | } 155 | } 156 | } 157 | 158 | #endregion Applying identity and UNC mappings 159 | 160 | $migrationTable.Save($writePath) 161 | $writePath 162 | } 163 | } -------------------------------------------------------------------------------- /GPOTools/internal/functions/Resolve-ADPrincipal.ps1: -------------------------------------------------------------------------------- 1 | function Resolve-ADPrincipal 2 | { 3 | <# 4 | .SYNOPSIS 5 | Resolves an AD Principal into a common format. 6 | 7 | .DESCRIPTION 8 | Resolves an AD Principal into a common format. 9 | Optimized for use with cross-domain migration procedures. 10 | 11 | Caches successful results. 12 | Returns empty values on unresolved users. 13 | 14 | .PARAMETER Name 15 | Name of the principal to resolve. 16 | 17 | .PARAMETER Domain 18 | Domain to resolve it for. 19 | Read access is required. 20 | 21 | .EXAMPLE 22 | PS C:\> Resolve-ADPrincipal -Name 'contoso\max' -Domain 'contoso.com' 23 | 24 | Resolves the user max from contoso.com 25 | #> 26 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '')] 27 | [CmdletBinding()] 28 | param ( 29 | [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] 30 | [string[]] 31 | $Name, 32 | 33 | [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 34 | [string] 35 | $Domain 36 | ) 37 | 38 | begin 39 | { 40 | if (-not $script:principals) { $script:principals = @{ } } 41 | 42 | $principalsToIgnore = @( 43 | # .NET Account sids, that are shared across all domains and need no translation 44 | 'S-1-5-82-3876422241-1344743610-1729199087-774402673-2621913236' 45 | 'S-1-5-82-271721585-897601226-2024613209-625570482-296978595' 46 | 47 | # Everyone, as it is 100% generic and has no domain-prefix 48 | 'S-1-1-0' 49 | 50 | # NT Authority SIDs, as SID-to-SID need no translation, localization can be an issue 51 | 'S-1-5-18' 52 | 'S-1-5-19' 53 | 'S-1-5-20' 54 | ) 55 | 56 | $defaultDomainData = Get-DomainData -Domain $Domain 57 | $defaultDomainFQDN = $defaultDomainData.Fqdn 58 | $defaultDomainName = $defaultDomainData.Name 59 | } 60 | process 61 | { 62 | foreach ($identity in $Name) 63 | { 64 | if ($identity -in $principalsToIgnore) { continue } 65 | 66 | Write-Verbose "[Resolve-ADPrincipal] Resolving $identity" 67 | 68 | #region Resolve Principal Domain 69 | $domainFQDN = $defaultDomainFQDN 70 | $domainName = $defaultDomainName 71 | if ($identity -like "*@*") 72 | { 73 | $domainObject = Get-DomainData -Domain $identity.Split("@")[1] 74 | if ($domainObject) 75 | { 76 | $domainFQDN = $domainObject.Fqdn 77 | $domainName = $domainObject.Name 78 | } 79 | } 80 | elseif ($identity -as [System.Security.Principal.SecurityIdentifier]) 81 | { 82 | if (([System.Security.Principal.SecurityIdentifier]$identity).AccountDomainSid) 83 | { 84 | $domainObject = Get-DomainData -Domain ([System.Security.Principal.SecurityIdentifier]$identity).AccountDomainSid 85 | if ($domainObject) 86 | { 87 | $domainFQDN = $domainObject.Fqdn 88 | $domainName = $domainObject.Name 89 | } 90 | } 91 | } 92 | elseif ($identity -like "*\*") 93 | { 94 | try { $domainObject = Get-DomainData -Domain $identity.Split("\")[0] -ErrorAction Stop } 95 | catch { } 96 | if ($domainObject) 97 | { 98 | $domainFQDN = $domainObject.Fqdn 99 | $domainName = $domainObject.Name 100 | } 101 | } 102 | $rootDomain = (Get-ADForest -Server $domainFQDN).RootDomain 103 | #endregion Resolve Principal Domain 104 | 105 | if (-not $script:principals[$domainFQDN]) { $script:principals[$domainFQDN] = @{ } } 106 | 107 | # Return form Cache if available 108 | if ($script:principals[$domainFQDN][$identity]) 109 | { 110 | return $script:principals[$domainFQDN][$identity] 111 | } 112 | 113 | #region Resolve User in AD 114 | if ($identity -as [System.Security.Principal.SecurityIdentifier]) 115 | { 116 | $adObject = Get-ADObject -Server $domainFQDN -LDAPFilter "(objectSID=$identity)" -Properties ObjectSID, SamAccountName 117 | # Handle Builtin SIDs that only exist in the root domain 118 | if (-not $adObject) { $adObject = Get-ADObject -Server $rootDomain -LDAPFilter "(objectSID=$identity)" -Properties ObjectSID, SamAccountName } 119 | } 120 | elseif (Test-IsDistinguishedName -Name $identity) 121 | { 122 | $adObject = Get-ADObject -Server ($identity | ConvertTo-DnsDomainName) -Identity $identity -Properties ObjectSID, SamAccountName 123 | } 124 | elseif ($identity -like "*\*") 125 | { 126 | try { $sidName = ([System.Security.Principal.NTAccount]$identity).Translate([System.Security.Principal.SecurityIdentifier]) } 127 | catch 128 | { 129 | Write-Warning "Failed to translate identity: $identity" 130 | continue 131 | } 132 | try { $adObject = Get-ADObject -Server $domainFQDN -LDAPFilter "(objectSID=$sidName)" -Properties ObjectSID, SamAccountName -ErrorAction Stop } 133 | catch { } 134 | if (-not $adObject) 135 | { 136 | $script:principals[$domainFQDN][$identity] = [pscustomobject]@{ 137 | DistinguishedName = $null 138 | Name = $identity 139 | SID = $sidName.Value 140 | RID = $sidName.Value.ToString().Split("-")[-1] 141 | Type = 'Local BuiltIn' 142 | IsBuiltin = $true 143 | DomainName = $domainName 144 | DomainFqdn = $domainFQDN 145 | } 146 | $script:principals[$domainFQDN][$identity] 147 | continue 148 | } 149 | } 150 | else 151 | { 152 | try 153 | { 154 | $sidName = ([System.Security.Principal.NTAccount]$identity).Translate([System.Security.Principal.SecurityIdentifier]) 155 | if ($sidName.Value -like 'S-1-3-*') 156 | { 157 | $script:principals[$domainFQDN][$identity] = [pscustomobject]@{ 158 | DistinguishedName = $null 159 | Name = $identity 160 | SID = $sidName.Value 161 | RID = $sidName.Value.ToString().Split("-")[-1] 162 | Type = 'Local BuiltIn' 163 | IsBuiltin = $true 164 | DomainName = $domainName 165 | DomainFqdn = $domainFQDN 166 | } 167 | $script:principals[$domainFQDN][$identity] 168 | continue 169 | } 170 | $adObject = Get-ADObject -Server $domainFQDN -LDAPFilter "(objectSID=$sidName)" -Properties ObjectSID, SamAccountName 171 | } 172 | catch 173 | { 174 | $adObject = Get-ADObject -Server $domainFQDN -LDAPFilter "(SamAccountName=$identity)" -Properties ObjectSID, SamAccountName 175 | } 176 | } 177 | if (-not $adObject -or -not $adObject.ObjectSID) 178 | { 179 | Write-Warning "Failed to resolve principal: $identity" 180 | continue 181 | } 182 | #endregion Resolve User in AD 183 | 184 | $script:principals[$domainFQDN][$identity] = [pscustomobject]@{ 185 | DistinguishedName = $adObject.DistinguishedName 186 | Name = $adObject.SamAccountName 187 | SID = $adObject.ObjectSID.Value 188 | RID = $adObject.ObjectSID.Value.ToString().Split("-")[-1] 189 | Type = $adObject.ObjectClass 190 | IsBuiltin = ((($adObject.ObjectSID.Value.Split("-")[-1] -as [int]) -lt 1000) -or ($adObject.ObjectSID.Value -like 'S-1-5-32-*')) 191 | DomainName = $domainName 192 | DomainFqdn = $domainFQDN 193 | } 194 | $script:principals[$domainFQDN][$identity] 195 | } 196 | } 197 | } -------------------------------------------------------------------------------- /GPOTools/internal/functions/Resolve-DomainMapping.ps1: -------------------------------------------------------------------------------- 1 | function Resolve-DomainMapping { 2 | <# 3 | .SYNOPSIS 4 | Resolves a source domain from a GPO export into domain of the destination domain. 5 | 6 | .DESCRIPTION 7 | Resolves a source domain from a GPO export into domain of the destination domain. 8 | The mapping data for this is managed by Register-GptDomainMapping. 9 | Usual source of mapping data is Import-GptDomainData and a scan of the destination forest. 10 | 11 | Accepts SID, Fqdn and Netbios Name as input to find the correct domain. 12 | Uses SID first, then Fqdn and only as a last resort the Netbios name, if all are specified. 13 | 14 | It returns an AD Domain object, representing the destination domain the source domain maps to. 15 | This object can be faked by the user, if manual data sources need to be included, 16 | but it is assumed, that such an object will also have all the data fields required. 17 | 18 | .PARAMETER DomainSid 19 | SID of the domain from the export source. 20 | 21 | .PARAMETER DomainFqdn 22 | Fqdn of the domain from the export source. 23 | 24 | .PARAMETER DomainName 25 | Name of the domain from the export source. 26 | 27 | .EXAMPLE 28 | PS C:\> Resolve-DomainMapping -DomainSid $identity.DomainSID -DomainFqdn $identity.DomainFqdn -DomainName $identity.DomainName 29 | 30 | Resolves the destination domain to map the specified identity to. 31 | Tries to use SID first, then FQDN and Netbios name only if nothing else worked. 32 | #> 33 | [CmdletBinding()] 34 | param ( 35 | [string] 36 | $DomainSid, 37 | 38 | [string] 39 | $DomainFqdn, 40 | 41 | [string] 42 | $DomainName 43 | ) 44 | 45 | if (-not $script:domainMapping) { 46 | throw "No domain mappings loaded yet. Run Import-GptDomainData or Register-GptDomainMapping to initialize the domain resolution table." 47 | } 48 | 49 | if ($DomainSid -and $script:domainMapping.Sid[$DomainSid]) { 50 | return $script:domainMapping.Sid[$DomainSid] 51 | } 52 | if ($DomainFqdn -and $script:domainMapping.FQDN[$DomainFqdn]) { 53 | return $script:domainMapping.FQDN[$DomainFqdn] 54 | } 55 | if ($DomainName -and $script:domainMapping.Name[$DomainName]) { 56 | return $script:domainMapping.Name[$DomainName] 57 | } 58 | 59 | throw "No matching domain found! ($DomainSid | $DomainFqdn | $DomainName)" 60 | } -------------------------------------------------------------------------------- /GPOTools/internal/functions/Test-IsDistinguishedName.ps1: -------------------------------------------------------------------------------- 1 | function Test-IsDistinguishedName 2 | { 3 | <# 4 | .SYNOPSIS 5 | Lightweight test to check whether a string is a distinguished name. 6 | 7 | .DESCRIPTION 8 | Lightweight test to check whether a string is a distinguished name. 9 | This check is done by checking, whether the string contains a "DC=" sequence. 10 | 11 | .PARAMETER Name 12 | The name to check. 13 | 14 | .EXAMPLE 15 | PS C:\> Test-IsDistinguishedName -Name $name 16 | 17 | returns whether $name is a distinguished name. 18 | #> 19 | [CmdletBinding()] 20 | param ( 21 | [Parameter(Mandatory = $true)] 22 | [string] 23 | $Name 24 | ) 25 | 26 | process 27 | { 28 | $Name -match 'DC=' 29 | } 30 | } -------------------------------------------------------------------------------- /GPOTools/internal/functions/Test-Overlap.ps1: -------------------------------------------------------------------------------- 1 | function Test-Overlap 2 | { 3 | <# 4 | .SYNOPSIS 5 | Matches N:N mappings for congruence. 6 | 7 | .DESCRIPTION 8 | Matches N:N mappings for congruence. 9 | Use this for comparing two arrays for overlap. 10 | This can be used for scenarios such as: 11 | - Whether n Items in Array One are equal to an Item in Array Two. 12 | - Whether n Items in Array One are similar to an Item in Array Two. 13 | This is especially designed to abstract filtering by multiple wildcard filters. 14 | 15 | .PARAMETER ReferenceObject 16 | The object(s) to compare 17 | 18 | .PARAMETER DifferenceObject 19 | The array of items to compare them to. 20 | 21 | .PARAMETER Property 22 | Compare a property, rather than the basic object. 23 | 24 | .PARAMETER Count 25 | The number of congruent items required for a successful result. 26 | Defaults to 1. 27 | 28 | .PARAMETER Operator 29 | How the comparison should be performed. 30 | Defaults to 'Equal' 31 | Supported Comparisons: Equal, Like, Match 32 | 33 | .EXAMPLE 34 | PS C:\> Test-Overlap -ReferenceObject $ReferenceObject -DifferenceObject $DifferenceObject 35 | 36 | Tests whether any item in the two arrays are equal. 37 | #> 38 | [OutputType([System.Boolean])] 39 | [CmdletBinding()] 40 | param ( 41 | [Parameter(Mandatory = $true)] 42 | [AllowNull()] 43 | $ReferenceObject, 44 | 45 | [Parameter(Mandatory = $true)] 46 | [AllowNull()] 47 | $DifferenceObject, 48 | 49 | [string] 50 | $Property, 51 | 52 | [int] 53 | $Count = 1, 54 | 55 | [ValidateSet('Equal', 'Like', 'Match')] 56 | [string] 57 | $Operator = 'Equal' 58 | ) 59 | 60 | begin 61 | { 62 | $parameter = @{ 63 | IncludeEqual = $true 64 | ExcludeDifferent = $true 65 | } 66 | if ($Property) { $parameter['Property'] = $Property } 67 | } 68 | process 69 | { 70 | switch ($Operator) 71 | { 72 | 'Equal' 73 | { 74 | return (Compare-Object -ReferenceObject $ReferenceObject -DifferenceObject $DebugPreference @parameter | Measure-Object).Count -ge $Count 75 | } 76 | 'Like' 77 | { 78 | $numberFound = 0 79 | foreach ($reference in $ReferenceObject) 80 | { 81 | foreach ($difference in $DifferenceObject) 82 | { 83 | if ($Property -and ($reference.$Property -like $difference.$Property)) { $numberFound++ } 84 | elseif (-not $Property -and ($reference -like $difference)) { $numberFound++ } 85 | 86 | if ($numberFound -ge $Count) { return $true } 87 | } 88 | } 89 | 90 | return $false 91 | } 92 | 'Match' 93 | { 94 | $numberFound = 0 95 | foreach ($reference in $ReferenceObject) 96 | { 97 | foreach ($difference in $DifferenceObject) 98 | { 99 | if ($Property -and ($reference.$Property -match $difference.$Property)) { $numberFound++ } 100 | elseif (-not $Property -and ($reference -match $difference)) { $numberFound++ } 101 | 102 | if ($numberFound -ge $Count) { return $true } 103 | } 104 | } 105 | 106 | return $false 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /GPOTools/internal/functions/Update-NetworkDrive.ps1: -------------------------------------------------------------------------------- 1 | function Update-NetworkDrive 2 | { 3 | <# 4 | .SYNOPSIS 5 | Remaps mapped network drives if needed. 6 | 7 | .DESCRIPTION 8 | Remaps mapped network drives if needed. 9 | Performs no operation, if no network drives are mapped on a GPO. 10 | Migration tables do not correctly update mapped drives, unfoortunately. 11 | 12 | Requires valid source data to be already imported, for example by running Import-GptDomainData or Import-GptIdentity. 13 | 14 | .PARAMETER GpoName 15 | Name of the GPO to update. 16 | 17 | .PARAMETER Domain 18 | The destination domain into which the GPO has been imported. 19 | 20 | .EXAMPLE 21 | PS C:\> Update-NetworkDrive -GpoName 'Share Y:' -Domain 'contoso.com' 22 | 23 | Updates the GPO "Share Y:" for the domain contoso.com, remapping the share from the source domain to the destination domain. 24 | #> 25 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] 26 | [CmdletBinding()] 27 | param ( 28 | [Parameter(Mandatory = $true)] 29 | [string] 30 | $GpoName, 31 | 32 | [Parameter(Mandatory = $true)] 33 | [string] 34 | $Domain 35 | ) 36 | 37 | begin 38 | { 39 | try 40 | { 41 | $gpoObject = Get-GPO -Domain $Domain -Name $GpoName -ErrorAction Stop 42 | $destinationDomain = (Get-DomainData -Domain $Domain).ADObject 43 | $gpoADObject = Get-ADObject -Server $destinationDomain.PDCEmulator -Identity $gpoObject.Path -Properties gPCFileSysPath -ErrorAction Stop 44 | } 45 | catch { throw } 46 | 47 | if ($script:sourceDomainData) 48 | { 49 | $sourceDomainDNS = $script:sourceDomainData.DomainDNSName 50 | $sourceDomainNetBios = $script:sourceDomainData.NetBIOSName 51 | } 52 | elseif ($script:identityMapping.Count -gt 0) 53 | { 54 | $sourceDomainDNS = $script:identityMapping[0].DomainFqdn 55 | $sourceDomainNetBios = $script:identityMapping[0].DomainName 56 | } 57 | else 58 | { 59 | throw "Unable to determine source domain. Run Import-GptDomainData or Import-GptIdentity first!" 60 | } 61 | } 62 | process 63 | { 64 | Write-Verbose "$GpoName : Processing Network Shares" 65 | $driveXmlPath = Join-Path -Path $gpoADObject.gPCFileSysPath -ChildPath 'User\Preferences\Drives\Drives.xml' 66 | if (-not (Test-Path -Path $driveXmlPath)) 67 | { 68 | Write-Verbose "$GpoName : Does not contain Network Shares" 69 | return 70 | } 71 | 72 | try { $driveString = Get-Content -Path $driveXmlPath -Raw -ErrorAction Stop -Encoding UTF8 } 73 | catch 74 | { 75 | Write-Verbose "$GpoName : Could not access Network Shares file" 76 | return 77 | } 78 | 79 | $driveStringNew = $driveString.Replace("\\$sourceDomainDNS\", "\\$($destinationDomain.DNSRoot)\").Replace("\\$sourceDomainNetBios\", "\\$($destinationDomain.NetBIOSName)\") 80 | 81 | if ($driveStringNew -eq $driveString) 82 | { 83 | Write-Verbose "$GpoName : Nothing to remap in the defined shares" 84 | return 85 | } 86 | 87 | try { Set-Content -Value $driveStringNew -Path $driveXmlPath -Encoding UTF8 -ErrorAction Stop } 88 | catch { throw } 89 | } 90 | } -------------------------------------------------------------------------------- /GPOTools/internal/functions/readme.md: -------------------------------------------------------------------------------- 1 | # Functions 2 | 3 | This is the folder where the internal functions go. 4 | 5 | Depending on the complexity of the module, it is recommended to subdivide them into subfolders. 6 | 7 | The module will pick up all .ps1 files recursively -------------------------------------------------------------------------------- /GPOTools/internal/scriptblocks/scriptblocks.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Stored scriptblocks are available in [PsfValidateScript()] attributes. 3 | This makes it easier to centrally provide the same scriptblock multiple times, 4 | without having to maintain it in separate locations. 5 | 6 | It also prevents lengthy validation scriptblocks from making your parameter block 7 | hard to read. 8 | 9 | Set-PSFScriptblock -Name 'GPOTools.ScriptBlockName' -Scriptblock { 10 | 11 | } 12 | #> -------------------------------------------------------------------------------- /GPOTools/internal/scripts/license.ps1: -------------------------------------------------------------------------------- 1 | New-PSFLicense -Product 'GPOTools' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2019-06-04") -Text @" 2 | Copyright (c) 2019 Friedrich Weinmann 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | "@ -------------------------------------------------------------------------------- /GPOTools/internal/scripts/postimport.ps1: -------------------------------------------------------------------------------- 1 | # Add all things you want to run after importing the main code 2 | <# 3 | # Load Configurations 4 | foreach ($file in (Get-ChildItem "$($script:ModuleRoot)\internal\configurations\*.ps1" -ErrorAction Ignore)) { 5 | . Import-ModuleFile -Path $file.FullName 6 | } 7 | 8 | # Load Scriptblocks 9 | foreach ($file in (Get-ChildItem "$($script:ModuleRoot)\internal\scriptblocks\*.ps1" -ErrorAction Ignore)) { 10 | . Import-ModuleFile -Path $file.FullName 11 | } 12 | 13 | # Load Tab Expansion 14 | foreach ($file in (Get-ChildItem "$($script:ModuleRoot)\internal\tepp\*.tepp.ps1" -ErrorAction Ignore)) { 15 | . Import-ModuleFile -Path $file.FullName 16 | } 17 | 18 | # Load Tab Expansion Assignment 19 | . Import-ModuleFile -Path "$($script:ModuleRoot)\internal\tepp\assignment.ps1" 20 | 21 | # Load License 22 | . Import-ModuleFile -Path "$($script:ModuleRoot)\internal\scripts\license.ps1" 23 | #> -------------------------------------------------------------------------------- /GPOTools/internal/scripts/preimport.ps1: -------------------------------------------------------------------------------- 1 | # Add all things you want to run before importing the main code 2 | 3 | # Load the strings used in messages 4 | # . Import-ModuleFile -Path "$($script:ModuleRoot)\internal\scripts\strings.ps1" -------------------------------------------------------------------------------- /GPOTools/internal/scripts/strings.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | This file loads the strings documents from the respective language folders. 3 | This allows localizing messages and errors. 4 | Load psd1 language files for each language you wish to support. 5 | Partial translations are acceptable - when missing a current language message, 6 | it will fallback to English or another available language. 7 | #> 8 | Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'GPOTools' -Language 'en-US' -------------------------------------------------------------------------------- /GPOTools/internal/tepp/assignment.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | # Example: 3 | Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name GPOTools.alcohol 4 | #> -------------------------------------------------------------------------------- /GPOTools/internal/tepp/example.tepp.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | # Example: 3 | Register-PSFTeppScriptblock -Name "GPOTools.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' } 4 | #> -------------------------------------------------------------------------------- /GPOTools/internal/tepp/readme.md: -------------------------------------------------------------------------------- 1 | # Tab Expansion 2 | 3 | ## Description 4 | 5 | Modern Tab Expansion was opened to users with the module `Tab Expansion Plus Plus` (TEPP). 6 | 7 | It allows you to define, what options a user is offered when tabbing through input options. This can save a lot of time for the user and is considered a key element in user experience. 8 | 9 | The `PSFramework` offers a simplified way of offering just this, as the two example files show. 10 | 11 | ## Concept 12 | 13 | Custom tab completion is defined in two steps: 14 | 15 | - Define a scriptblock that is run when the user hits `TAB` and provides the strings that are his options. 16 | - Assign that scriptblock to the parameter of a command. You can assign the same scriptblock multiple times. 17 | 18 | ## Structure 19 | 20 | Import order matters. In order to make things work with the default scaffold, follow those rules: 21 | 22 | - All scriptfiles _defining_ completion scriptblocks like this: `*.tepp.ps1` 23 | - Put all your completion assignments in `assignment.ps1` -------------------------------------------------------------------------------- /GPOTools/readme.md: -------------------------------------------------------------------------------- 1 | # PSFModule guidance 2 | 3 | This is a finished module layout optimized for implementing the PSFramework. 4 | 5 | If you don't care to deal with the details, this is what you need to do to get started seeing results: 6 | 7 | - Add the functions you want to publish to `/functions/` 8 | - Update the `FunctionsToExport` node in the module manifest (GPOTools.psd1). All functions you want to publish should be in a list. 9 | - Add internal helper functions the user should not see to `/internal/functions/` 10 | 11 | ## Path Warning 12 | 13 | > If you want your module to be compatible with Linux and MacOS, keep in mind that those OS are case sensitive for paths and files. 14 | 15 | `Import-ModuleFile` is preconfigured to resolve the path of the files specified, so it will reliably convert weird path notations the system can't handle. 16 | Content imported through that command thus need not mind the path separator. 17 | If you want to make sure your code too will survive OS-specific path notations, get used to using `Resolve-path` or the more powerful `Resolve-PSFPath`. -------------------------------------------------------------------------------- /GPOTools/tests/functions/readme.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | This is where the function tests go. 4 | 5 | Make sure to put them in folders reflecting the actual module structure. 6 | 7 | It is not necessary to differentiate between internal and public functions here. -------------------------------------------------------------------------------- /GPOTools/tests/general/FileIntegrity.Exceptions.ps1: -------------------------------------------------------------------------------- 1 | # List of forbidden commands 2 | $global:BannedCommands = @( 3 | 'Write-Host', 4 | # 'Write-Verbose', # Until PSFramework implementation 5 | # 'Write-Warning', # Until PSFramework implementation 6 | # 'Write-Error', # Until PSFramework implementation 7 | 'Write-Output', 8 | 'Write-Information', 9 | 'Write-Debug', 10 | 11 | # Use CIM instead where possible 12 | 'Get-WmiObject', 13 | 'Invoke-WmiMethod', 14 | 'Register-WmiEvent', 15 | 'Remove-WmiObject', 16 | 'Set-WmiInstance' 17 | ) 18 | 19 | <# 20 | Contains list of exceptions for banned cmdlets. 21 | Insert the file names of files that may contain them. 22 | 23 | Example: 24 | "Write-Host" = @('Write-PSFHostColor.ps1','Write-PSFMessage.ps1') 25 | #> 26 | $global:MayContainCommand = @{ 27 | "Write-Host" = @() 28 | "Write-Verbose" = @() 29 | "Write-Warning" = @() 30 | "Write-Error" = @() 31 | "Write-Output" = @() 32 | "Write-Information" = @() 33 | "Write-Debug" = @() 34 | } -------------------------------------------------------------------------------- /GPOTools/tests/general/FileIntegrity.Tests.ps1: -------------------------------------------------------------------------------- 1 | $moduleRoot = (Resolve-Path "$global:testroot\..").Path 2 | 3 | . "$global:testroot\general\FileIntegrity.Exceptions.ps1" 4 | 5 | Describe "Verifying integrity of module files" { 6 | BeforeAll { 7 | function Get-FileEncoding 8 | { 9 | <# 10 | .SYNOPSIS 11 | Tests a file for encoding. 12 | 13 | .DESCRIPTION 14 | Tests a file for encoding. 15 | 16 | .PARAMETER Path 17 | The file to test 18 | #> 19 | [CmdletBinding()] 20 | Param ( 21 | [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] 22 | [Alias('FullName')] 23 | [string] 24 | $Path 25 | ) 26 | 27 | if ($PSVersionTable.PSVersion.Major -lt 6) 28 | { 29 | [byte[]]$byte = get-content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $Path 30 | } 31 | else 32 | { 33 | [byte[]]$byte = Get-Content -AsByteStream -ReadCount 4 -TotalCount 4 -Path $Path 34 | } 35 | 36 | if ($byte[0] -eq 0xef -and $byte[1] -eq 0xbb -and $byte[2] -eq 0xbf) { 'UTF8 BOM' } 37 | elseif ($byte[0] -eq 0xfe -and $byte[1] -eq 0xff) { 'Unicode' } 38 | elseif ($byte[0] -eq 0 -and $byte[1] -eq 0 -and $byte[2] -eq 0xfe -and $byte[3] -eq 0xff) { 'UTF32' } 39 | elseif ($byte[0] -eq 0x2b -and $byte[1] -eq 0x2f -and $byte[2] -eq 0x76) { 'UTF7' } 40 | else { 'Unknown' } 41 | } 42 | } 43 | 44 | Context "Validating PS1 Script files" { 45 | $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.ps1" | Where-Object FullName -NotLike "$moduleRoot\tests\*" 46 | 47 | foreach ($file in $allFiles) 48 | { 49 | $name = $file.FullName.Replace("$moduleRoot\", '') 50 | 51 | It "[$name] Should have UTF8 encoding with Byte Order Mark" -TestCases @{ file = $file } { 52 | Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' 53 | } 54 | 55 | It "[$name] Should have no trailing space" -TestCases @{ file = $file } { 56 | ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0}).LineNumber | Should -BeNullOrEmpty 57 | } 58 | 59 | $tokens = $null 60 | $parseErrors = $null 61 | $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors) 62 | 63 | It "[$name] Should have no syntax errors" -TestCases @{ parseErrors = $parseErrors } { 64 | $parseErrors | Should -BeNullOrEmpty 65 | } 66 | 67 | foreach ($command in $global:BannedCommands) 68 | { 69 | if ($global:MayContainCommand["$command"] -notcontains $file.Name) 70 | { 71 | It "[$name] Should not use $command" -TestCases @{ tokens = $tokens; command = $command } { 72 | $tokens | Where-Object Text -EQ $command | Should -BeNullOrEmpty 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | Context "Validating help.txt help files" { 80 | $allFiles = Get-ChildItem -Path $moduleRoot -Recurse | Where-Object Name -like "*.help.txt" | Where-Object FullName -NotLike "$moduleRoot\tests\*" 81 | 82 | foreach ($file in $allFiles) 83 | { 84 | $name = $file.FullName.Replace("$moduleRoot\", '') 85 | 86 | It "[$name] Should have UTF8 encoding" -TestCases @{ file = $file } { 87 | Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8 BOM' 88 | } 89 | 90 | It "[$name] Should have no trailing space" -TestCases @{ file = $file } { 91 | ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0 } | Measure-Object).Count | Should -Be 0 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /GPOTools/tests/general/Help.Exceptions.ps1: -------------------------------------------------------------------------------- 1 | # List of functions that should be ignored 2 | $global:FunctionHelpTestExceptions = @( 3 | 4 | ) 5 | 6 | <# 7 | List of arrayed enumerations. These need to be treated differently. Add full name. 8 | Example: 9 | 10 | "Sqlcollaborative.Dbatools.Connection.ManagementConnectionType[]" 11 | #> 12 | $global:HelpTestEnumeratedArrays = @( 13 | 14 | ) 15 | 16 | <# 17 | Some types on parameters just fail their validation no matter what. 18 | For those it becomes possible to skip them, by adding them to this hashtable. 19 | Add by following this convention: = @() 20 | Example: 21 | 22 | "Get-DbaCmObject" = @("DoNotUse") 23 | #> 24 | $global:HelpTestSkipParameterType = @{ 25 | 26 | } 27 | -------------------------------------------------------------------------------- /GPOTools/tests/general/Help.Tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .NOTES 3 | The original test this is based upon was written by June Blender. 4 | After several rounds of modifications it stands now as it is, but the honor remains hers. 5 | 6 | Thank you June, for all you have done! 7 | 8 | .DESCRIPTION 9 | This test evaluates the help for all commands in a module. 10 | 11 | .PARAMETER SkipTest 12 | Disables this test. 13 | 14 | .PARAMETER CommandPath 15 | List of paths under which the script files are stored. 16 | This test assumes that all functions have their own file that is named after themselves. 17 | These paths are used to search for commands that should exist and be tested. 18 | Will search recursively and accepts wildcards, make sure only functions are found 19 | 20 | .PARAMETER ModuleName 21 | Name of the module to be tested. 22 | The module must already be imported 23 | 24 | .PARAMETER ExceptionsFile 25 | File in which exceptions and adjustments are configured. 26 | In it there should be two arrays and a hashtable defined: 27 | $global:FunctionHelpTestExceptions 28 | $global:HelpTestEnumeratedArrays 29 | $global:HelpTestSkipParameterType 30 | These can be used to tweak the tests slightly in cases of need. 31 | See the example file for explanations on each of these usage and effect. 32 | #> 33 | [CmdletBinding()] 34 | Param ( 35 | [switch] 36 | $SkipTest, 37 | 38 | [string[]] 39 | $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions"), 40 | 41 | [string] 42 | $ModuleName = "GPOTools", 43 | 44 | [string] 45 | $ExceptionsFile = "$global:testroot\general\Help.Exceptions.ps1" 46 | ) 47 | if ($SkipTest) { return } 48 | . $ExceptionsFile 49 | 50 | $includedNames = (Get-ChildItem $CommandPath -Recurse -File | Where-Object Name -like "*.ps1").BaseName 51 | $commands = Get-Command -Module (Get-Module $ModuleName) -CommandType Cmdlet, Function, Workflow | Where-Object Name -in $includedNames 52 | 53 | ## When testing help, remember that help is cached at the beginning of each session. 54 | ## To test, restart session. 55 | 56 | 57 | foreach ($command in $commands) { 58 | $commandName = $command.Name 59 | 60 | # Skip all functions that are on the exclusions list 61 | if ($global:FunctionHelpTestExceptions -contains $commandName) { continue } 62 | 63 | # The module-qualified command fails on Microsoft.PowerShell.Archive cmdlets 64 | $Help = Get-Help $commandName -ErrorAction SilentlyContinue 65 | 66 | Describe "Test help for $commandName" { 67 | 68 | # If help is not found, synopsis in auto-generated help is the syntax diagram 69 | It "should not be auto-generated" -TestCases @{ Help = $Help } { 70 | $Help.Synopsis | Should -Not -BeLike '*`[``]*' 71 | } 72 | 73 | # Should be a description for every function 74 | It "gets description for $commandName" -TestCases @{ Help = $Help } { 75 | $Help.Description | Should -Not -BeNullOrEmpty 76 | } 77 | 78 | # Should be at least one example 79 | It "gets example code from $commandName" -TestCases @{ Help = $Help } { 80 | ($Help.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty 81 | } 82 | 83 | # Should be at least one example description 84 | It "gets example help from $commandName" -TestCases @{ Help = $Help } { 85 | ($Help.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty 86 | } 87 | 88 | Context "Test parameter help for $commandName" { 89 | 90 | $common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable' 91 | 92 | $parameters = $command.ParameterSets.Parameters | Sort-Object -Property Name -Unique | Where-Object Name -notin $common 93 | $parameterNames = $parameters.Name 94 | $HelpParameterNames = $Help.Parameters.Parameter.Name | Sort-Object -Unique 95 | foreach ($parameter in $parameters) { 96 | $parameterName = $parameter.Name 97 | $parameterHelp = $Help.parameters.parameter | Where-Object Name -EQ $parameterName 98 | 99 | # Should be a description for every parameter 100 | It "gets help for parameter: $parameterName : in $commandName" -TestCases @{ parameterHelp = $parameterHelp } { 101 | $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty 102 | } 103 | 104 | $codeMandatory = $parameter.IsMandatory.toString() 105 | It "help for $parameterName parameter in $commandName has correct Mandatory value" -TestCases @{ parameterHelp = $parameterHelp; codeMandatory = $codeMandatory } { 106 | $parameterHelp.Required | Should -Be $codeMandatory 107 | } 108 | 109 | if ($HelpTestSkipParameterType[$commandName] -contains $parameterName) { continue } 110 | 111 | $codeType = $parameter.ParameterType.Name 112 | 113 | if ($parameter.ParameterType.IsEnum) { 114 | # Enumerations often have issues with the typename not being reliably available 115 | $names = $parameter.ParameterType::GetNames($parameter.ParameterType) 116 | # Parameter type in Help should match code 117 | It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } { 118 | $parameterHelp.parameterValueGroup.parameterValue | Should -be $names 119 | } 120 | } 121 | elseif ($parameter.ParameterType.FullName -in $HelpTestEnumeratedArrays) { 122 | # Enumerations often have issues with the typename not being reliably available 123 | $names = [Enum]::GetNames($parameter.ParameterType.DeclaredMembers[0].ReturnType) 124 | It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ parameterHelp = $parameterHelp; names = $names } { 125 | $parameterHelp.parameterValueGroup.parameterValue | Should -be $names 126 | } 127 | } 128 | else { 129 | # To avoid calling Trim method on a null object. 130 | $helpType = if ($parameterHelp.parameterValue) { $parameterHelp.parameterValue.Trim() } 131 | # Parameter type in Help should match code 132 | It "help for $commandName has correct parameter type for $parameterName" -TestCases @{ helpType = $helpType; codeType = $codeType } { 133 | $helpType | Should -be $codeType 134 | } 135 | } 136 | } 137 | foreach ($helpParm in $HelpParameterNames) { 138 | # Shouldn't find extra parameters in help. 139 | It "finds help parameter in code: $helpParm" -TestCases @{ helpParm = $helpParm; parameterNames = $parameterNames } { 140 | $helpParm -in $parameterNames | Should -Be $true 141 | } 142 | } 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /GPOTools/tests/general/Manifest.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe "Validating the module manifest" { 2 | $moduleRoot = (Resolve-Path "$global:testroot\..").Path 3 | $manifest = ((Get-Content "$moduleRoot\GPOTools.psd1") -join "`n") | Invoke-Expression 4 | Context "Basic resources validation" { 5 | $files = Get-ChildItem "$moduleRoot\functions" -Recurse -File | Where-Object Name -like "*.ps1" 6 | It "Exports all functions in the public folder" -TestCases @{ files = $files; manifest = $manifest } { 7 | 8 | $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '<=').InputObject 9 | $functions | Should -BeNullOrEmpty 10 | } 11 | It "Exports no function that isn't also present in the public folder" -TestCases @{ files = $files; manifest = $manifest } { 12 | $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '=>').InputObject 13 | $functions | Should -BeNullOrEmpty 14 | } 15 | 16 | It "Exports none of its internal functions" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } { 17 | $files = Get-ChildItem "$moduleRoot\internal\functions" -Recurse -File -Filter "*.ps1" 18 | $files | Where-Object BaseName -In $manifest.FunctionsToExport | Should -BeNullOrEmpty 19 | } 20 | } 21 | 22 | Context "Individual file validation" { 23 | It "The root module file exists" -TestCases @{ moduleRoot = $moduleRoot; manifest = $manifest } { 24 | Test-Path "$moduleRoot\$($manifest.RootModule)" | Should -Be $true 25 | } 26 | 27 | foreach ($format in $manifest.FormatsToProcess) 28 | { 29 | It "The file $format should exist" -TestCases @{ moduleRoot = $moduleRoot; format = $format } { 30 | Test-Path "$moduleRoot\$format" | Should -Be $true 31 | } 32 | } 33 | 34 | foreach ($type in $manifest.TypesToProcess) 35 | { 36 | It "The file $type should exist" -TestCases @{ moduleRoot = $moduleRoot; type = $type } { 37 | Test-Path "$moduleRoot\$type" | Should -Be $true 38 | } 39 | } 40 | 41 | foreach ($assembly in $manifest.RequiredAssemblies) 42 | { 43 | if ($assembly -like "*.dll") { 44 | It "The file $assembly should exist" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } { 45 | Test-Path "$moduleRoot\$assembly" | Should -Be $true 46 | } 47 | } 48 | else { 49 | It "The file $assembly should load from the GAC" -TestCases @{ moduleRoot = $moduleRoot; assembly = $assembly } { 50 | { Add-Type -AssemblyName $assembly } | Should -Not -Throw 51 | } 52 | } 53 | } 54 | 55 | foreach ($tag in $manifest.PrivateData.PSData.Tags) 56 | { 57 | It "Tags should have no spaces in name" -TestCases @{ tag = $tag } { 58 | $tag -match " " | Should -Be $false 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /GPOTools/tests/general/PSScriptAnalyzer.Tests.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param ( 3 | [switch] 4 | $SkipTest, 5 | 6 | [string[]] 7 | $CommandPath = @("$global:testroot\..\functions", "$global:testroot\..\internal\functions") 8 | ) 9 | 10 | if ($SkipTest) { return } 11 | 12 | $global:__pester_data.ScriptAnalyzer = New-Object System.Collections.ArrayList 13 | 14 | Describe 'Invoking PSScriptAnalyzer against commandbase' { 15 | $commandFiles = Get-ChildItem -Path $CommandPath -Recurse | Where-Object Name -like "*.ps1" 16 | $scriptAnalyzerRules = Get-ScriptAnalyzerRule 17 | 18 | foreach ($file in $commandFiles) 19 | { 20 | Context "Analyzing $($file.BaseName)" { 21 | $analysis = Invoke-ScriptAnalyzer -Path $file.FullName -ExcludeRule PSAvoidTrailingWhitespace, PSShouldProcess 22 | 23 | forEach ($rule in $scriptAnalyzerRules) 24 | { 25 | It "Should pass $rule" -TestCases @{ analysis = $analysis; rule = $rule } { 26 | If ($analysis.RuleName -contains $rule) 27 | { 28 | $analysis | Where-Object RuleName -EQ $rule -outvariable failures | ForEach-Object { $null = $global:__pester_data.ScriptAnalyzer.Add($_) } 29 | 30 | 1 | Should -Be 0 31 | } 32 | else 33 | { 34 | 0 | Should -Be 0 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /GPOTools/tests/pester.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | $TestGeneral = $true, 3 | 4 | $TestFunctions = $true, 5 | 6 | [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')] 7 | [Alias('Show')] 8 | $Output = "None", 9 | 10 | $Include = "*", 11 | 12 | $Exclude = "" 13 | ) 14 | 15 | Write-PSFMessage -Level Important -Message "Starting Tests" 16 | 17 | Write-PSFMessage -Level Important -Message "Importing Module" 18 | 19 | $global:testroot = $PSScriptRoot 20 | $global:__pester_data = @{ } 21 | 22 | Remove-Module GPOTools -ErrorAction Ignore 23 | Import-Module "$PSScriptRoot\..\GPOTools.psd1" 24 | Import-Module "$PSScriptRoot\..\GPOTools.psm1" -Force 25 | 26 | # Need to import explicitly so we can use the configuration class 27 | Import-Module Pester 28 | 29 | Write-PSFMessage -Level Important -Message "Creating test result folder" 30 | $null = New-Item -Path "$PSScriptRoot\..\.." -Name TestResults -ItemType Directory -Force 31 | 32 | $totalFailed = 0 33 | $totalRun = 0 34 | 35 | $testresults = @() 36 | $config = [PesterConfiguration]::Default 37 | $config.TestResult.Enabled = $true 38 | 39 | #region Run General Tests 40 | if ($TestGeneral) 41 | { 42 | Write-PSFMessage -Level Important -Message "Modules imported, proceeding with general tests" 43 | foreach ($file in (Get-ChildItem "$PSScriptRoot\general" | Where-Object Name -like "*.Tests.ps1")) 44 | { 45 | if ($file.Name -notlike $Include) { continue } 46 | if ($file.Name -like $Exclude) { continue } 47 | 48 | Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" 49 | $config.TestResult.OutputPath = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" 50 | $config.Run.Path = $file.FullName 51 | $config.Run.PassThru = $true 52 | $config.Output.Verbosity = $Output 53 | $results = Invoke-Pester -Configuration $config 54 | foreach ($result in $results) 55 | { 56 | $totalRun += $result.TotalCount 57 | $totalFailed += $result.FailedCount 58 | $result.Tests | Where-Object Result -ne 'Passed' | ForEach-Object { 59 | $testresults += [pscustomobject]@{ 60 | Block = $_.Block 61 | Name = "It $($_.Name)" 62 | Result = $_.Result 63 | Message = $_.ErrorRecord.DisplayErrorMessage 64 | } 65 | } 66 | } 67 | } 68 | } 69 | #endregion Run General Tests 70 | 71 | $global:__pester_data.ScriptAnalyzer | Out-Host 72 | 73 | #region Test Commands 74 | if ($TestFunctions) 75 | { 76 | Write-PSFMessage -Level Important -Message "Proceeding with individual tests" 77 | foreach ($file in (Get-ChildItem "$PSScriptRoot\functions" -Recurse -File | Where-Object Name -like "*Tests.ps1")) 78 | { 79 | if ($file.Name -notlike $Include) { continue } 80 | if ($file.Name -like $Exclude) { continue } 81 | 82 | Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" 83 | $config.TestResult.OutputPath = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" 84 | $config.Run.Path = $file.FullName 85 | $config.Run.PassThru = $true 86 | $config.Output.Verbosity = $Output 87 | $results = Invoke-Pester -Configuration $config 88 | foreach ($result in $results) 89 | { 90 | $totalRun += $result.TotalCount 91 | $totalFailed += $result.FailedCount 92 | $result.Tests | Where-Object Result -ne 'Passed' | ForEach-Object { 93 | $testresults += [pscustomobject]@{ 94 | Block = $_.Block 95 | Name = "It $($_.Name)" 96 | Result = $_.Result 97 | Message = $_.ErrorRecord.DisplayErrorMessage 98 | } 99 | } 100 | } 101 | } 102 | } 103 | #endregion Test Commands 104 | 105 | $testresults | Sort-Object Describe, Context, Name, Result, Message | Format-List 106 | 107 | if ($totalFailed -eq 0) { Write-PSFMessage -Level Critical -Message "All $totalRun tests executed without a single failure!" } 108 | else { Write-PSFMessage -Level Critical -Message "$totalFailed tests out of $totalRun tests failed!" } 109 | 110 | if ($totalFailed -gt 0) 111 | { 112 | throw "$totalFailed / $totalRun tests failed!" 113 | } -------------------------------------------------------------------------------- /GPOTools/tests/readme.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | This is the folder, where all the tests go. 4 | 5 | Those are subdivided in two categories: 6 | 7 | - General 8 | - Function 9 | 10 | ## General Tests 11 | 12 | General tests are function generic and test for general policies. 13 | 14 | These test scan answer questions such as: 15 | 16 | - Is my module following my style guides? 17 | - Does any of my scripts have a syntax error? 18 | - Do my scripts use commands I do not want them to use? 19 | - Do my commands follow best practices? 20 | - Do my commands have proper help? 21 | 22 | Basically, these allow a general module health check. 23 | 24 | These tests are already provided as part of the template. 25 | 26 | ## Function Tests 27 | 28 | A healthy module should provide unit and integration tests for the commands & components it ships. 29 | Only then can be guaranteed, that they will actually perform as promised. 30 | 31 | However, as each such test must be specific to the function it tests, there cannot be much in the way of templates. -------------------------------------------------------------------------------- /GPOTools/xml/GPOTools.Format.ps1xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | GPOTools.ImportResult 7 | 8 | GPOTools.ImportResult 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Action 23 | 24 | 25 | Step 26 | 27 | 28 | Success 29 | 30 | 31 | Target 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /GPOTools/xml/GPOTools.Types.ps1xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Deserialized.Foo.Bar 6 | 7 | 8 | PSStandardMembers 9 | 10 | 11 | 12 | TargetTypeForDeserialization 13 | 14 | 15 | Foo.Bar 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Foo.Bar 24 | 25 | 26 | SerializationData 27 | 28 | PSFramework.Serialization.SerializationTypeConverter 29 | GetSerializationData 30 | 31 | 32 | 33 | 34 | PSFramework.Serialization.SerializationTypeConverter 35 | 36 | 37 | -------------------------------------------------------------------------------- /GPOTools/xml/readme.md: -------------------------------------------------------------------------------- 1 | # XML 2 | 3 | This is the folder where project XML files go, notably: 4 | 5 | - Format XML 6 | - Type Extension XML 7 | 8 | External help files should _not_ be placed in this folder! 9 | 10 | ## Notes on Files and Naming 11 | 12 | There should be only one format file and one type extension file per project, as importing them has a notable impact on import times. 13 | 14 | - The Format XML should be named `GPOTools.Format.ps1xml` 15 | - The Type Extension XML should be named `GPOTools.Types.ps1xml` 16 | 17 | ## Tools 18 | 19 | ### New-PSMDFormatTableDefinition 20 | 21 | This function will take an input object and generate format xml for an auto-sized table. 22 | 23 | It provides a simple way to get started with formats. 24 | 25 | ### Get-PSFTypeSerializationData 26 | 27 | ``` 28 | C# Warning! 29 | This section is only interest if you're using C# together with PowerShell. 30 | ``` 31 | 32 | This function generates type extension XML that allows PowerShell to convert types written in C# to be written to file and restored from it without being 'Deserialized'. Also works for jobs or remoting, if both sides have the `PSFramework` module and type extension loaded. 33 | 34 | In order for a class to be eligible for this, it needs to conform to the following rules: 35 | 36 | - Have the `[Serializable]` attribute 37 | - Be public 38 | - Have an empty constructor 39 | - Allow all public properties/fields to be set (even if setting it doesn't do anything) without throwing an exception. 40 | 41 | ``` 42 | non-public properties and fields will be lost in this process! 43 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Friedrich Weinmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | The GPOTools module is designed to handle all things GPO. 4 | 5 | As a special focus, it tries to manage migrations, backup & restore. 6 | 7 | Compared to the built-in tools, its backup&restore capability also includes: 8 | 9 | - WMI Filter 10 | - GP Links & Link Order 11 | - Localized / Renamed builtin accounts & groups mapping 12 | - Customizable identity mapping 13 | - Permissions 14 | 15 | # Examples 16 | ## Installing the module 17 | 18 | ```powershell 19 | Install-Module GPOTools 20 | ``` 21 | 22 | ## Backup 23 | 24 | ```powershell 25 | # Backup ALL GPOs 26 | Backup-GptPolicy -Path . 27 | 28 | # Backup just the ones you want 29 | Get-GPO -All | Where-Object $condition | Backup-GptPolicy -Path . 30 | 31 | # Backup all policies that fit your desired name pattern 32 | Backup-GptPolicy -Path . -Name 'SEC-*' 33 | ``` 34 | 35 | ## Restore 36 | 37 | ```powershell 38 | # Restore everything 39 | Restore-GptPolicy -Path . 40 | 41 | # Restore just those policies you care about 42 | Restore-GptPolicy -Path . -Name 'SEC-*', 'Client-*' 43 | 44 | # Restore while mapping groups from the source domain to different groups in the destination domain 45 | Restore-GptPolicy -Path . -IdentityMapping @{ 46 | 'S-D-FileServerAdmins' = 'SD1-FSAdmins' 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /azFunctionResources/clientModule/function.ps1: -------------------------------------------------------------------------------- 1 | function %functionname% 2 | { 3 | %parameter% 4 | 5 | process 6 | { 7 | $invokeParameters = Get-InternalConnectionData -Method '%method%' -Parameter $PSBoundParameters -FunctionName '%condensedname%' 8 | Invoke-RestMethod @invokeParameters | ConvertFrom-PSFClixml 9 | } 10 | } -------------------------------------------------------------------------------- /azFunctionResources/clientModule/functions/Connect-GPOTools.ps1: -------------------------------------------------------------------------------- 1 | function Connect-GPOTools 2 | { 3 | <# 4 | .SYNOPSIS 5 | Configures the connection to the GPOTools Azure Function. 6 | 7 | .DESCRIPTION 8 | Configures the connection to the GPOTools Azure Function. 9 | 10 | .PARAMETER Uri 11 | Url to connect to the GPOTools Azure function. 12 | 13 | .PARAMETER UnprotectedToken 14 | The unencrypted access token to the GPOTools Azure function. ONLY use this from secure locations or non-sensitive functions! 15 | 16 | .PARAMETER ProtectedToken 17 | An encrypted access token to the GPOTools Azure function. Use this to persist an access token in a way only the current user on the current system can access. 18 | 19 | .PARAMETER Register 20 | Using this command, the module will remember the connection settings persistently across PowerShell sessions. 21 | CAUTION: When using unencrypted token data (such as specified through the -UnprotectedToken parameter), the authenticating token will be stored in clear-text! 22 | 23 | .EXAMPLE 24 | PS C:\> Connect-GPOTools -Uri 'https://demofunctionapp.azurewebsites.net/api/' 25 | 26 | Establishes a connection to GPOTools 27 | #> 28 | [CmdletBinding()] 29 | param ( 30 | [string] 31 | $Uri, 32 | 33 | [string] 34 | $UnprotectedToken, 35 | 36 | [System.Management.Automation.PSCredential] 37 | $ProtectedToken, 38 | 39 | [switch] 40 | $Register 41 | ) 42 | 43 | process 44 | { 45 | if (Test-PSFParameterBinding -ParameterName UnprotectedToken) 46 | { 47 | Set-PSFConfig -Module 'GPOTools' -Name 'Client.UnprotectedToken' -Value $UnprotectedToken 48 | if ($Register) { Register-PSFConfig -Module 'GPOTools' -Name 'Client.UnprotectedToken' } 49 | } 50 | if (Test-PSFParameterBinding -ParameterName Uri) 51 | { 52 | Set-PSFConfig -Module 'GPOTools' -Name 'Client.Uri' -Value $Uri 53 | if ($Register) { Register-PSFConfig -Module 'GPOTools' -Name 'Client.Uri' } 54 | } 55 | if (Test-PSFParameterBinding -ParameterName ProtectedToken) 56 | { 57 | Set-PSFConfig -Module 'GPOTools' -Name 'Client.ProtectedToken' -Value $ProtectedToken 58 | if ($Register) { Register-PSFConfig -Module 'GPOTools' -Name 'Client.ProtectedToken' } 59 | } 60 | 61 | } 62 | } -------------------------------------------------------------------------------- /azFunctionResources/clientModule/internal/configurations/connection.ps1: -------------------------------------------------------------------------------- 1 | Set-PSFConfig -Module 'GPOTools' -Name 'Client.Uri' -Value $null -Initialize -Validation 'string' -Description "Url to connect to the GPOTools Azure function" 2 | Set-PSFConfig -Module 'GPOTools' -Name 'Client.UnprotectedToken' -Value '' -Initialize -Validation 'string' -Description "The unencrypted access token to the GPOTools Azure function. ONLY use this from secure locations or non-sensitive functions!" 3 | Set-PSFConfig -Module 'GPOTools' -Name 'Client.ProtectedToken' -Value $null -Initialize -Validation 'credential' -Description "An encrypted access token to the GPOTools Azure function. Use this to persist an access token in a way only the current user on the current system can access." -------------------------------------------------------------------------------- /azFunctionResources/clientModule/internal/functions/Get-InternalConnectionData.ps1: -------------------------------------------------------------------------------- 1 | function Get-InternalConnectionData 2 | { 3 | <# 4 | .SYNOPSIS 5 | Creates parameter hashtables for Invoke-RestMethod calls. 6 | 7 | .DESCRIPTION 8 | Creates parameter hashtables for Invoke-RestMethod calls. 9 | This is the main abstraction layer for public functions. 10 | 11 | .PARAMETER Method 12 | The Rest Method to use when calling this function. 13 | 14 | .PARAMETER Parameters 15 | The PSBoundParameters object. Will be passed online using PowerShell Serialization. 16 | 17 | .PARAMETER FunctionName 18 | The name of the Azure Function to call. 19 | This should always be the condensed name of the function. 20 | #> 21 | [CmdletBinding()] 22 | param ( 23 | [string] 24 | $Method, 25 | 26 | $Parameters, 27 | 28 | [string] 29 | $FunctionName 30 | ) 31 | 32 | process 33 | { 34 | try { $uri = '{0}{1}' -f (Get-PSFConfigValue -FullName 'GPOTools.Client.Uri' -NotNull), $FunctionName } 35 | catch { $PSCmdlet.ThrowTerminatingError($_) } 36 | $header = @{ } 37 | 38 | #region Authentication 39 | $unprotectedToken = Get-PSFConfigValue -FullName 'GPOTools.Client.UnprotectedToken' 40 | $protectedToken = Get-PSFConfigValue -FullName 'GPOTools.Client.ProtectedToken' 41 | 42 | $authenticationDone = $false 43 | if ($protectedToken -and -not $authenticationDone) 44 | { 45 | $uri += '?code={0}' -f $protectedToken.GetNetworkCredential().Password 46 | $authenticationDone = $true 47 | } 48 | if ($unprotectedToken -and -not $authenticationDone) 49 | { 50 | $uri += '?code={0}' -f $unprotectedToken 51 | $authenticationDone = $true 52 | } 53 | if (-not $authenticationDone) 54 | { 55 | throw "No Authentication configured!" 56 | } 57 | #endregion Authentication 58 | 59 | 60 | @{ 61 | Method = $Method 62 | Uri = $uri 63 | Headers = $header 64 | Body = (@{ 65 | __SerializedParameters = ($Parameters | ConvertTo-PSFHashtable | ConvertTo-PSFClixml) 66 | __PSSerialize = $true 67 | } | ConvertTo-Json) 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /azFunctionResources/clientModule/moduleroot.psm1: -------------------------------------------------------------------------------- 1 | $script:ModuleRoot = $PSScriptRoot 2 | 3 | foreach ($file in (Get-ChildItem -Path "$script:ModuleRoot\internal\configurations" -Recurse -Filter '*.ps1')) 4 | { 5 | . $file.FullName 6 | } 7 | foreach ($file in (Get-ChildItem -Path "$script:ModuleRoot\internal\functions" -Recurse -Filter '*.ps1')) 8 | { 9 | . $file.FullName 10 | } 11 | foreach ($file in (Get-ChildItem -Path "$script:ModuleRoot\functions" -Recurse -Filter '*.ps1')) 12 | { 13 | . $file.FullName 14 | } -------------------------------------------------------------------------------- /azFunctionResources/functionOverride/Get-Example.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "Request", 8 | "methods": [ 9 | "delete" 10 | ] 11 | }, 12 | { 13 | "type": "http", 14 | "direction": "out", 15 | "name": "Response" 16 | } 17 | ], 18 | "disabled": true 19 | } -------------------------------------------------------------------------------- /azFunctionResources/functionOverride/Get-Example.ps1: -------------------------------------------------------------------------------- 1 | function Get-Example 2 | { 3 | <# 4 | .NOTES 5 | This file will be used to override the auto-generated CLIENT MODULE function implementation. 6 | 7 | Using the vsts-createFunctionClientModule.ps1 task script, you can autogenerate a client module. 8 | That module can be used to connect to the published Azure Function Module. 9 | However sometimes you may want to override the default client function for a given command, 10 | in order to better customize the way it behaves. 11 | 12 | Creating a ps1 file with the name of the specific function in this folder will use this file, 13 | rather than creating a default copy. 14 | 15 | NOTE: 16 | There will be no further automatic change detection! 17 | If you later update the Azure Function, you need to manually update the client function as well. 18 | #> 19 | [CmdletBinding()] 20 | Param ( 21 | 22 | ) 23 | 24 | begin 25 | { 26 | 27 | } 28 | process 29 | { 30 | 31 | } 32 | end 33 | { 34 | 35 | } 36 | } -------------------------------------------------------------------------------- /azFunctionResources/functionOverride/Get-Example.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | # Override the rest methods used for the API endpoint 3 | # RestMethods = 'delete' 4 | 5 | # Override inclusion into client module 6 | # NoClientFunction 7 | } -------------------------------------------------------------------------------- /azFunctionResources/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /azFunctionResources/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "FUNCTIONS_WORKER_RUNTIME": "powershell", 5 | "AzureWebJobsStorage": "--connection string for storage account---" 6 | } 7 | } -------------------------------------------------------------------------------- /azFunctionResources/profile.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | This is the globl profile file for the Azure Function App. 3 | This file will have been executed first, before any function runs. 4 | Use this to create a common execution environment, 5 | but keep in mind that the profile execution time is added to the function startup time for ALL functions. 6 | #> 7 | 8 | Set-PSFConfig -FullName 'AutogeneratedAzureFunction.Function.StatusCode' -Value ([System.Net.HttpStatusCode]::OK) -------------------------------------------------------------------------------- /azFunctionResources/profileFunctions/Convert-AzureFunctionParameter.ps1: -------------------------------------------------------------------------------- 1 | function Convert-AzureFunctionParameter 2 | { 3 | <# 4 | .SYNOPSIS 5 | Extracts the parameters passed into the rest method. 6 | 7 | .DESCRIPTION 8 | Extracts the parameters passed into the rest method of an Azure Function. 9 | Returns a hashtable, similar to what would be found on a $PSBoundParameters variable. 10 | 11 | .PARAMETER Request 12 | The request to process 13 | 14 | .EXAMPLE 15 | PS C:\> Convert-AzureFunctionParameter -Request $request 16 | 17 | Converts the $request object into a regular hashtable. 18 | #> 19 | [OutputType([System.Collections.Hashtable])] 20 | [CmdletBinding()] 21 | param ( 22 | $Request 23 | ) 24 | 25 | $parameterObject = [pscustomobject]@{ 26 | Parameters = @{ } 27 | Serialize = $false 28 | } 29 | 30 | foreach ($key in $Request.Query.Keys) 31 | { 32 | # Do NOT include the authentication key 33 | if ($key -eq 'code') { continue } 34 | $parameterObject.Parameters[$key] = $Request.Query.$key 35 | } 36 | foreach ($key in $Request.Body.Keys) 37 | { 38 | $parameterObject.Parameters[$key] = $Request.Body.$key 39 | } 40 | if ($parameterObject.Parameters.__PSSerialize) 41 | { 42 | $parameterObject.Serialize = $true 43 | $null = $parameterObject.Parameters.Remove('__PSSerialize') 44 | } 45 | if ($parameterObject.Parameters.__SerializedParameters) 46 | { 47 | $parameterObject.Parameters = $parameterObject.Parameters.__SerializedParameters | ConvertFrom-PSFClixml 48 | } 49 | 50 | $parameterObject 51 | } -------------------------------------------------------------------------------- /azFunctionResources/profileFunctions/Set-AzureFunctionStatus.ps1: -------------------------------------------------------------------------------- 1 | function Set-AzureFunctionStatus 2 | { 3 | <# 4 | .SYNOPSIS 5 | Sets the return status of the function. 6 | 7 | .DESCRIPTION 8 | Sets the return status of the function. 9 | By default, the status is "OK" 10 | 11 | .PARAMETER Status 12 | Set the HTTP status for the return from Azure Functions. 13 | Any status other than OK will cause a terminating error if run outside of Azure Functions. 14 | 15 | .EXAMPLE 16 | PS C:\> Set-AzureFunctionStatus -Status BadRequest 17 | 18 | Updates the status to say "BadRequest" 19 | #> 20 | [CmdletBinding()] 21 | param ( 22 | [Parameter(Mandatory = $true)] 23 | [System.Net.HttpStatusCode] 24 | $Status 25 | ) 26 | 27 | Set-PSFConfig -FullName 'AutogeneratedAzureFunction.Function.StatusCode' -Value $Status 28 | } -------------------------------------------------------------------------------- /azFunctionResources/profileFunctions/Write-AzureFunctionOutput.ps1: -------------------------------------------------------------------------------- 1 | function Write-AzureFunctionOutput 2 | { 3 | <# 4 | .SYNOPSIS 5 | Write output equally well from Azure Functions or locally. 6 | 7 | .DESCRIPTION 8 | Write output equally well from Azure Functions or locally. 9 | When calling this command, call return straight after it. 10 | Use Write-AzureFunctionStatus first if an error should be returned, then specify an error text here. 11 | 12 | .PARAMETER Value 13 | The value data to return. 14 | Either an error message 15 | 16 | .PARAMETER Serialize 17 | Return the output object as compressed clixml string. 18 | You can use ConvertFrom-PSFClixml to restore the object on the recipient-side. 19 | 20 | .EXAMPLE 21 | PS C:\> Write-AzureFunctionOutput -Value $result 22 | 23 | Writes the content of $result as output. 24 | 25 | .EXAMPLE 26 | PS C:\> Write-AzureFunctionOutput -Value $result -Serialize 27 | 28 | Writes the content of $result as output. 29 | If called from Azure Functions, it will convert the output as compressed clixml string. 30 | 31 | #> 32 | [CmdletBinding()] 33 | param ( 34 | [Parameter(Mandatory = $true)] 35 | $Value, 36 | 37 | [switch] 38 | $Serialize, 39 | 40 | [System.Net.HttpStatusCode] 41 | $Status 42 | ) 43 | 44 | if ($PSBoundParameters.ContainsKey('Status')) 45 | { 46 | Set-AzureFunctionStatus -Status $Status 47 | } 48 | 49 | if ($Serialize) 50 | { 51 | $Value = $Value | ConvertTo-PSFClixml 52 | } 53 | 54 | Push-OutputBinding -Name Response -Value ( 55 | [HttpResponseContext]@{ 56 | StatusCode = (Get-PSFConfigValue -FullName 'AutogeneratedAzureFunction.Function.StatusCode' -Fallback ([System.Net.HttpStatusCode]::OK)) 57 | Body = $Value 58 | } 59 | ) 60 | } -------------------------------------------------------------------------------- /azFunctionResources/readme.md: -------------------------------------------------------------------------------- 1 | # Azure Function Resources 2 | 3 | This folder is used to store Azure Function specific meta data and resources. 4 | 5 | This folder is also used to allow the user to easily create a custom function-specific configuration, for exanmple in order to change the trigger settings. 6 | 7 | To specify custom, 'Per Function' configuration json, just place the desired configuration file as 'functionname.json' into this folder (it does not matter if it is the PowerShell function name or the condensed version used for publishing on Azure). -------------------------------------------------------------------------------- /azFunctionResources/run.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | $Request, 3 | 4 | $TriggerMetadata 5 | ) 6 | 7 | $parameterObject = Convert-AzureFunctionParameter -Request $Request 8 | $parameters = $parameterObject.Parameters 9 | try { $data = %functionname% @parameters } 10 | catch 11 | { 12 | Write-AzureFunctionOutput -Value "Failed to execute: $_" -Status InternalServerError 13 | return 14 | } 15 | 16 | Write-AzureFunctionOutput -Value $data -Serialize:$parameterObject.Serialize -------------------------------------------------------------------------------- /build/AzureFunction.readme.md: -------------------------------------------------------------------------------- 1 | # Setting up the release pipeline: 2 | 3 | ## Preliminary 4 | 5 | Setting up a release pipeline, set the trigger to do continuous integration against the master branch only. 6 | In Stage 1 set up a tasksequence: 7 | 8 | ## 1) PowerShell Task: Prerequisites 9 | 10 | Have it execute `vsts-prerequisites.ps1` 11 | 12 | ## 2) PowerShell Task: Validate 13 | 14 | Have it execute `vsts-prerequisites.ps1` 15 | 16 | ## 3) PowerShell Task: Build 17 | 18 | Have it execute `vsts-build.ps1`. 19 | The task requires two parameters: 20 | 21 | - `-LocalRepo` 22 | - `-WorkingDirectory $(System.DefaultWorkingDirectory)/_�name�` 23 | 24 | ## 4) Publish Test Results 25 | 26 | Configure task to pick up nunit type of tests (rather than the default junit). 27 | Configure task to execute, even if previous steps failed or the task sequence was cancelled. 28 | 29 | ## 5) PowerShell Task: Package Function 30 | 31 | Have it execute `vsts-packageFunction.ps1` 32 | 33 | ## 6) Azure Function AppDeploy 34 | 35 | Configure to publish to the correct function app. -------------------------------------------------------------------------------- /build/filesAfter.txt: -------------------------------------------------------------------------------- 1 | # List all files that are loaded in the postimport.ps1 2 | # In the order they are loaded during postimport 3 | 4 | # internal\configurations\*.ps1 5 | # internal\scriptblocks\*.ps1 6 | # internal\tepp\*.tepp.ps1 7 | # internal\tepp\assignment.ps1 8 | # internal\scripts\license.ps1 -------------------------------------------------------------------------------- /build/filesBefore.txt: -------------------------------------------------------------------------------- 1 | # List all files that are loaded in the preimport.ps1 2 | # In the order they are loaded during preimport 3 | 4 | # internal\scripts\strings.ps1 -------------------------------------------------------------------------------- /build/vsts-build.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | This script publishes the module to the gallery. 3 | It expects as input an ApiKey authorized to publish the module. 4 | 5 | Insert any build steps you may need to take before publishing it here. 6 | #> 7 | param ( 8 | $ApiKey, 9 | 10 | $WorkingDirectory, 11 | 12 | $Repository = 'PSGallery', 13 | 14 | [switch] 15 | $LocalRepo, 16 | 17 | [switch] 18 | $SkipPublish, 19 | 20 | [switch] 21 | $AutoVersion 22 | ) 23 | 24 | #region Handle Working Directory Defaults 25 | if (-not $WorkingDirectory) 26 | { 27 | if ($env:RELEASE_PRIMARYARTIFACTSOURCEALIAS) 28 | { 29 | $WorkingDirectory = Join-Path -Path $env:SYSTEM_DEFAULTWORKINGDIRECTORY -ChildPath $env:RELEASE_PRIMARYARTIFACTSOURCEALIAS 30 | } 31 | else { $WorkingDirectory = $env:SYSTEM_DEFAULTWORKINGDIRECTORY } 32 | } 33 | #endregion Handle Working Directory Defaults 34 | 35 | # Prepare publish folder 36 | Write-PSFMessage -Level Important -Message "Creating and populating publishing directory" 37 | $publishDir = New-Item -Path $WorkingDirectory -Name publish -ItemType Directory 38 | Copy-Item -Path "$($WorkingDirectory)\GPOTools" -Destination $publishDir.FullName -Recurse -Force 39 | 40 | #region Gather text data to compile 41 | $text = @() 42 | $processed = @() 43 | 44 | # Gather Stuff to run before 45 | foreach ($line in (Get-Content "$($PSScriptRoot)\filesBefore.txt" | Where-Object { $_ -notlike "#*" })) 46 | { 47 | if ([string]::IsNullOrWhiteSpace($line)) { continue } 48 | 49 | $basePath = Join-Path "$($publishDir.FullName)\GPOTools" $line 50 | foreach ($entry in (Resolve-PSFPath -Path $basePath)) 51 | { 52 | $item = Get-Item $entry 53 | if ($item.PSIsContainer) { continue } 54 | if ($item.FullName -in $processed) { continue } 55 | $text += [System.IO.File]::ReadAllText($item.FullName) 56 | $processed += $item.FullName 57 | } 58 | } 59 | 60 | # Gather commands 61 | Get-ChildItem -Path "$($publishDir.FullName)\GPOTools\internal\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object { 62 | $text += [System.IO.File]::ReadAllText($_.FullName) 63 | } 64 | Get-ChildItem -Path "$($publishDir.FullName)\GPOTools\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object { 65 | $text += [System.IO.File]::ReadAllText($_.FullName) 66 | } 67 | 68 | # Gather stuff to run afterwards 69 | foreach ($line in (Get-Content "$($PSScriptRoot)\filesAfter.txt" | Where-Object { $_ -notlike "#*" })) 70 | { 71 | if ([string]::IsNullOrWhiteSpace($line)) { continue } 72 | 73 | $basePath = Join-Path "$($publishDir.FullName)\GPOTools" $line 74 | foreach ($entry in (Resolve-PSFPath -Path $basePath)) 75 | { 76 | $item = Get-Item $entry 77 | if ($item.PSIsContainer) { continue } 78 | if ($item.FullName -in $processed) { continue } 79 | $text += [System.IO.File]::ReadAllText($item.FullName) 80 | $processed += $item.FullName 81 | } 82 | } 83 | #endregion Gather text data to compile 84 | 85 | #region Update the psm1 file 86 | $fileData = Get-Content -Path "$($publishDir.FullName)\GPOTools\GPOTools.psm1" -Raw 87 | $fileData = $fileData.Replace('""', '""') 88 | $fileData = $fileData.Replace('""', ($text -join "`n`n")) 89 | [System.IO.File]::WriteAllText("$($publishDir.FullName)\GPOTools\GPOTools.psm1", $fileData, [System.Text.Encoding]::UTF8) 90 | #endregion Update the psm1 file 91 | 92 | #region Updating the Module Version 93 | if ($AutoVersion) 94 | { 95 | Write-PSFMessage -Level Important -Message "Updating module version numbers." 96 | try { [version]$remoteVersion = (Find-Module 'GPOTools' -Repository $Repository -ErrorAction Stop).Version } 97 | catch 98 | { 99 | Stop-PSFFunction -Message "Failed to access $($Repository)" -EnableException $true -ErrorRecord $_ 100 | } 101 | if (-not $remoteVersion) 102 | { 103 | Stop-PSFFunction -Message "Couldn't find GPOTools on repository $($Repository)" -EnableException $true 104 | } 105 | $newBuildNumber = $remoteVersion.Build + 1 106 | [version]$localVersion = (Import-PowerShellDataFile -Path "$($publishDir.FullName)\GPOTools\GPOTools.psd1").ModuleVersion 107 | Update-ModuleManifest -Path "$($publishDir.FullName)\GPOTools\GPOTools.psd1" -ModuleVersion "$($localVersion.Major).$($localVersion.Minor).$($newBuildNumber)" 108 | } 109 | #endregion Updating the Module Version 110 | 111 | #region Publish 112 | if ($SkipPublish) { return } 113 | if ($LocalRepo) 114 | { 115 | # Dependencies must go first 116 | Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: PSFramework" 117 | New-PSMDModuleNugetPackage -ModulePath (Get-Module -Name PSFramework).ModuleBase -PackagePath . 118 | Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: GPOTools" 119 | New-PSMDModuleNugetPackage -ModulePath "$($publishDir.FullName)\GPOTools" -PackagePath . 120 | } 121 | else 122 | { 123 | # Publish to Gallery 124 | Write-PSFMessage -Level Important -Message "Publishing the GPOTools module to $($Repository)" 125 | Publish-Module -Path "$($publishDir.FullName)\GPOTools" -NuGetApiKey $ApiKey -Force -Repository $Repository 126 | } 127 | #endregion Publish -------------------------------------------------------------------------------- /build/vsts-createFunctionClientModule.ps1: -------------------------------------------------------------------------------- 1 |  2 | <# 3 | .SYNOPSIS 4 | Build script that generates a client module for REST API endpoints of a Azure PowerShell Functions project. 5 | 6 | .DESCRIPTION 7 | Build script that generates a client module for REST API endpoints of a Azure PowerShell Functions project. 8 | 9 | .PARAMETER ApiKey 10 | The API key to use to publish the module to a Nuget Repository 11 | 12 | .PARAMETER WorkingDirectory 13 | The root folder from which to build the module. 14 | 15 | .PARAMETER Repository 16 | The name of the repository to publish to. 17 | Defaults to PSGallery. 18 | 19 | .PARAMETER LocalRepo 20 | Instead of publishing to a gallery, drop a nuget package in the root folder. 21 | This package can then be picked up in a later step for publishing to Azure Artifacts. 22 | 23 | .PARAMETER ModuleName 24 | The name to give to the client module. 25 | By default, the client module will be named '.Client'. 26 | 27 | .PARAMETER IncludeFormat 28 | Include the format xml of the source module for the client module. 29 | 30 | .PARAMETER IncludeType 31 | Include the type extension xml of the source module for the client module. 32 | 33 | .PARAMETER IncludeAssembly 34 | Include the binaries of the source module for the client module. 35 | #> 36 | param ( 37 | $ApiKey, 38 | 39 | $WorkingDirectory, 40 | 41 | $Repository = 'PSGallery', 42 | 43 | [switch] 44 | $LocalRepo, 45 | 46 | $ModuleName, 47 | 48 | [switch] 49 | $IncludeFormat, 50 | 51 | [switch] 52 | $IncludeType, 53 | 54 | [switch] 55 | $IncludeAssembly 56 | ) 57 | 58 | #region Handle Working Directory Defaults 59 | if (-not $WorkingDirectory) 60 | { 61 | if ($env:RELEASE_PRIMARYARTIFACTSOURCEALIAS) 62 | { 63 | $WorkingDirectory = Join-Path -Path $env:SYSTEM_DEFAULTWORKINGDIRECTORY -ChildPath $env:RELEASE_PRIMARYARTIFACTSOURCEALIAS 64 | } 65 | else { $WorkingDirectory = $env:SYSTEM_DEFAULTWORKINGDIRECTORY } 66 | } 67 | #endregion Handle Working Directory Defaults 68 | 69 | Write-PSFMessage -Level Host -Message 'Starting Build: Client Module' 70 | $parentModule = 'GPOTools' 71 | if (-not $ModuleName) { $ModuleName = 'GPOTools.Client' } 72 | Write-PSFMessage -Level Host -Message 'Creating Folder Structure' 73 | $workingRoot = New-Item -Path $WorkingDirectory -Name $ModuleName -ItemType Directory 74 | $publishRoot = Join-Path -Path $WorkingDirectory -ChildPath 'publish\GPOTools' 75 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\clientModule\functions" -Destination "$($workingRoot.FullName)\" -Recurse 76 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\clientModule\internal" -Destination "$($workingRoot.FullName)\" -Recurse 77 | Copy-Item -Path "$($publishRoot)\en-us" -Destination "$($workingRoot.FullName)\" -Recurse 78 | $functionFolder = Get-Item -Path "$($workingRoot.FullName)\functions" 79 | 80 | #region Create Functions 81 | $encoding = [PSFEncoding]'utf8' 82 | $functionsText = Get-Content -Path "$($WorkingDirectory)\azFunctionResources\clientModule\function.ps1" -Raw 83 | 84 | Write-PSFMessage -Level Host -Message 'Creating Functions' 85 | foreach ($functionSourceFile in (Get-ChildItem -Path "$($publishRoot)\functions" -Recurse -Filter '*.ps1')) 86 | { 87 | Write-PSFMessage -Level Host -Message " Processing function: $($functionSourceFile.BaseName)" 88 | $condensedName = $functionSourceFile.BaseName -replace '-', '' 89 | 90 | #region Load Overrides 91 | $override = @{ } 92 | if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).psd1") 93 | { 94 | $override = Import-PowerShellDataFile -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).psd1" 95 | } 96 | if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).psd1") 97 | { 98 | $override = Import-PowerShellDataFile -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).psd1" 99 | } 100 | if ($override.NoClientFunction) 101 | { 102 | Write-PSFMessage -Level Host -Message " Override 'NoClientFunction' detected, skipping!" 103 | continue 104 | } 105 | 106 | # If there is an definition override, use it and continue 107 | if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).ps1") 108 | { 109 | Write-PSFMessage -Level Host -Message " Override function definition detected, using override" 110 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).ps1" -Destination $functionFolder.FullName 111 | continue 112 | } 113 | 114 | # Figure out the Rest Method to use 115 | $methodName = 'Post' 116 | if ($override.RestMethods) 117 | { 118 | $methodName = $override.RestMethods | Where-Object { $_ -ne 'Get' } | Select-Object -First 1 119 | } 120 | 121 | #endregion Load Overrides 122 | 123 | $currentFunctionsText = $functionsText -replace '%functionname%', $functionSourceFile.BaseName -replace '%condensedname%', $condensedName -replace '%method%', $methodName 124 | 125 | $parsedFunction = Read-PSMDScript -Path $functionSourceFile.FullName 126 | $functionAst = $parsedFunction.Ast.EndBlock.Statements | Where-Object { 127 | $_ -is [System.Management.Automation.Language.FunctionDefinitionAst] 128 | } | Select-Object -First 1 129 | 130 | $end = $functionAst.Body.ParamBlock.Extent.EndOffSet 131 | $start = $functionAst.Body.Extent.StartOffSet + 1 132 | $currentFunctionsText = $currentFunctionsText.Replace('%parameter%', $functionAst.Body.Extent.Text.SubString(1, ($end - $start))) 133 | 134 | Write-PSFMessage -Level Host -Message " Creating file: $($functionFolder.FullName)\$($functionSourceFile.Name)" 135 | [System.IO.File]::WriteAllText("$($functionFolder.FullName)\$($functionSourceFile.Name)", $currentFunctionsText, $encoding) 136 | } 137 | $functionsToExport = (Get-ChildItem -Path $functionFolder.FullName -Recurse -Filter *.ps1).BaseName | Sort-Object 138 | #endregion Create Functions 139 | 140 | #region Create Core Module Files 141 | # Get Manifest of published version, in order to catch build-phase changes such as module version. 142 | $originalManifestData = Import-PowerShellDataFile -Path "$publishRoot\GPOTools.psd1" 143 | $prereqHash = @{ 144 | ModuleName = 'PSFramework' 145 | ModuleVersion = (Get-Module PSFramework).Version 146 | } 147 | $paramNewModuleManifest = @{ 148 | Path = ('{0}\{1}.psd1' -f $workingRoot.FullName, $ModuleName) 149 | FunctionsToExport = $functionsToExport 150 | CompanyName = $originalManifestData.CompanyName 151 | Author = $originalManifestData.Author 152 | Description = $originalManifestData.Description 153 | ModuleVersion = $originalManifestData.ModuleVersion 154 | RootModule = ('{0}.psm1' -f $ModuleName) 155 | Copyright = $originalManifestData.Copyright 156 | TypesToProcess = @() 157 | FormatsToProcess = @() 158 | RequiredAssemblies = @() 159 | RequiredModules = @($prereqHash) 160 | CompatiblePSEditions = 'Core', 'Desktop' 161 | PowerShellVersion = '5.1' 162 | } 163 | 164 | if ($IncludeAssembly) { $paramNewModuleManifest.RequiredAssemblies = $originalManifestData.RequiredAssemblies } 165 | if ($IncludeFormat) { $paramNewModuleManifest.FormatsToProcess = $originalManifestData.FormatsToProcess } 166 | if ($IncludeType) { $paramNewModuleManifest.TypesToProcess = $originalManifestData.TypesToProcess } 167 | Write-PSFMessage -Level Host -Message "Creating Module Manifest for module: $ModuleName" 168 | New-ModuleManifest @paramNewModuleManifest 169 | 170 | Write-PSFMessage -Level Host -Message "Copying additional module files" 171 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\clientModule\moduleroot.psm1" -Destination "$($workingRoot.FullName)\$($ModuleName).psm1" 172 | Copy-Item -Path "$($WorkingDirectory)\LICENSE" -Destination "$($workingRoot.FullName)\" 173 | #endregion Create Core Module Files 174 | 175 | #region Transfer Additional Content 176 | if ($IncludeAssembly) 177 | { 178 | Copy-Item -Path "$publishRoot\bin" -Destination "$($workingRoot.FullName)\" -Recurse 179 | } 180 | if ($IncludeFormat -or $IncludeType) 181 | { 182 | Copy-Item -Path "$publishRoot\xml" -Destination "$($workingRoot.FullName)\" -Recurse 183 | } 184 | #endregion Transfer Additional Content 185 | 186 | #region Publish 187 | if ($LocalRepo) 188 | { 189 | # Dependencies must go first 190 | Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: PSFramework" 191 | New-PSMDModuleNugetPackage -ModulePath (Get-Module -Name PSFramework).ModuleBase -PackagePath . -WarningAction SilentlyContinue 192 | Write-PSFMessage -Level Important -Message "Creating Nuget Package for module: GPOTools" 193 | New-PSMDModuleNugetPackage -ModulePath $workingRoot.FullName -PackagePath . -EnableException 194 | } 195 | else 196 | { 197 | # Publish to Gallery 198 | Write-PSFMessage -Level Important -Message "Publishing the GPOTools module to $($Repository)" 199 | Publish-Module -Path $workingRoot.FullName -NuGetApiKey $ApiKey -Force -Repository $Repository 200 | } 201 | #endregion Publish -------------------------------------------------------------------------------- /build/vsts-packageFunction.ps1: -------------------------------------------------------------------------------- 1 |  2 | <# 3 | .SYNOPSIS 4 | Packages a Azure Functions project, ready to release. 5 | 6 | .DESCRIPTION 7 | Packages a Azure Functions project, ready to release. 8 | Should be part of the release pipeline, after ensuring validation. 9 | 10 | Look into the 'AzureFunctionRest' template for generating functions for the module if you do. 11 | 12 | .PARAMETER WorkingDirectory 13 | The root folder to work from. 14 | 15 | .PARAMETER Repository 16 | The name of the repository to use for gathering dependencies from. 17 | #> 18 | param ( 19 | $WorkingDirectory = "$($env:SYSTEM_DEFAULTWORKINGDIRECTORY)\_GPOTools", 20 | 21 | $Repository = 'PSGallery' 22 | ) 23 | 24 | $moduleName = 'GPOTools' 25 | 26 | # Prepare Paths 27 | Write-PSFMessage -Level Host -Message "Creating working folders" 28 | $moduleRoot = Join-Path -Path $WorkingDirectory -ChildPath 'publish' 29 | $workingRoot = New-Item -Path $WorkingDirectory -Name 'working' -ItemType Directory 30 | $modulesFolder = New-Item -Path $workingRoot.FullName -Name Modules -ItemType Directory 31 | 32 | # Fill out the modules folder 33 | Write-PSFMessage -Level Host -Message "Transfering built module data into working directory" 34 | Copy-Item -Path "$moduleRoot\$moduleName" -Destination $modulesFolder.FullName -Recurse -Force 35 | foreach ($dependency in (Import-PowerShellDataFile -Path "$moduleRoot\$moduleName\$moduleName.psd1").RequiredModules) 36 | { 37 | $param = @{ 38 | Repository = $Repository 39 | Name = $dependency.ModuleName 40 | Path = $modulesFolder.FullName 41 | } 42 | if ($dependency -is [string]) { $param['Name'] = $dependency } 43 | if ($dependency.RequiredVersion) 44 | { 45 | $param['RequiredVersion'] = $dependency.RequiredVersion 46 | } 47 | Write-PSFMessage -Level Host -Message "Preparing Dependency: $($param['Name'])" 48 | Save-Module @param 49 | } 50 | 51 | # Generate function configuration 52 | Write-PSFMessage -Level Host -Message 'Generating function configuration' 53 | $runTemplate = Get-Content -Path "$($WorkingDirectory)\azFunctionResources\run.ps1" -Raw 54 | foreach ($functionSourceFile in (Get-ChildItem -Path "$($moduleRoot)\$moduleName\functions" -Recurse -Filter '*.ps1')) 55 | { 56 | Write-PSFMessage -Level Host -Message " Processing function: $functionSourceFile" 57 | $condensedName = $functionSourceFile.BaseName -replace '-', '' 58 | $functionFolder = New-Item -Path $workingRoot.FullName -Name $condensedName -ItemType Directory 59 | 60 | #region Load Overrides 61 | $override = @{ } 62 | if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).psd1") 63 | { 64 | $override = Import-PowerShellDataFile -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).psd1" 65 | } 66 | if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).psd1") 67 | { 68 | $override = Import-PowerShellDataFile -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).psd1" 69 | } 70 | #endregion Load Overrides 71 | 72 | #region Create Function Configuration 73 | $restMethods = 'get', 'post' 74 | if ($override.RestMethods) { $restMethods = $override.RestMethods } 75 | 76 | Set-Content -Path "$($functionFolder.FullName)\function.json" -Value @" 77 | { 78 | "bindings": [ 79 | { 80 | "authLevel": "function", 81 | "type": "httpTrigger", 82 | "direction": "in", 83 | "name": "Request", 84 | "methods": [ 85 | "$($restMethods -join "`", 86 | `"")" 87 | ] 88 | }, 89 | { 90 | "type": "http", 91 | "direction": "out", 92 | "name": "Response" 93 | } 94 | ], 95 | "disabled": false 96 | } 97 | "@ 98 | #endregion Create Function Configuration 99 | 100 | #region Override Function Configuration 101 | if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).json") 102 | { 103 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($functionSourceFile.BaseName).json" -Destination "$($functionFolder.FullName)\function.json" -Force 104 | } 105 | if (Test-Path -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).json") 106 | { 107 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\functionOverride\$($condensedName).json" -Destination "$($functionFolder.FullName)\function.json" -Force 108 | } 109 | #endregion Override Function Configuration 110 | 111 | # Generate the run.ps1 file 112 | $runText = $runTemplate -replace '%functionname%', $functionSourceFile.BaseName 113 | $runText | Set-Content -Path "$($functionFolder.FullName)\run.ps1" -Encoding UTF8 114 | } 115 | 116 | # Transfer common files 117 | Write-PSFMessage -Level Host -Message "Transfering core function data" 118 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\host.json" -Destination "$($workingroot.FullName)\" 119 | Copy-Item -Path "$($WorkingDirectory)\azFunctionResources\local.settings.json" -Destination "$($workingroot.FullName)\" 120 | 121 | # Build the profile file 122 | $text = @() 123 | $text += Get-Content -Path "$($WorkingDirectory)\azFunctionResources\profile.ps1" -Raw 124 | foreach ($functionFile in (Get-ChildItem "$($WorkingDirectory)\azFunctionResources\profileFunctions" -Recurse)) 125 | { 126 | $text += Get-Content -Path $functionFile.FullName -Raw 127 | } 128 | $text -join "`n`n" | Set-Content "$($workingroot.FullName)\profile.ps1" 129 | 130 | # Zip It 131 | Write-PSFMessage -Level Host -Message "Creating function archive in '$($WorkingDirectory)\$moduleName.zip'" 132 | Compress-Archive -Path "$($workingroot.FullName)\*" -DestinationPath "$($WorkingDirectory)\$moduleName.zip" -Force -------------------------------------------------------------------------------- /build/vsts-prerequisites.ps1: -------------------------------------------------------------------------------- 1 | $modules = @("Pester", "PSFramework", "PSModuleDevelopment", "PSScriptAnalyzer", 'PackageManagement', 'PowerShellGet') 2 | 3 | foreach ($module in $modules) { 4 | Write-Host "Installing $module" -ForegroundColor Cyan 5 | Install-Module $module -Force -SkipPublisherCheck 6 | Import-Module $module -Force -PassThru 7 | } -------------------------------------------------------------------------------- /build/vsts-validate.ps1: -------------------------------------------------------------------------------- 1 | # Guide for available variables and working with secrets: 2 | # https://docs.microsoft.com/en-us/vsts/build-release/concepts/definitions/build/variables?tabs=powershell 3 | 4 | # Needs to ensure things are Done Right and only legal commits to master get built 5 | 6 | # Run internal pester tests 7 | & "$PSScriptRoot\..\GPOTools\tests\pester.ps1" -------------------------------------------------------------------------------- /labdefinitions/gpoMigration.lab.ps1: -------------------------------------------------------------------------------- 1 | $labname = 'GPOMigration' 2 | $labSources = 'C:\LabSources' 3 | $imageUI = 'Windows Server 2019 Datacenter (Desktop Experience)' 4 | $imageNoUI = 'Windows Server 2019 Datacenter' 5 | 6 | $forest1 = 'contoso.com' 7 | $forest1_subdomain = 'country.contoso.com' 8 | $forest2 = 'fabrikam.com' 9 | $forest2_subdomain = 'department.fabrikam.com' 10 | 11 | #region Utility Functions 12 | function New-LabChildDomain 13 | { 14 | [CmdletBinding()] 15 | param ( 16 | [string] 17 | $ComputerName, 18 | 19 | [string] 20 | $DnsServer, 21 | 22 | [string] 23 | $ParentForest, 24 | 25 | [string] 26 | $DomainName 27 | ) 28 | 29 | Set-LabDnsServer -ClientVMName $ComputerName -ServerVMName $DnsServer 30 | Invoke-LabCommand -ActivityName "Creating Child Domain: $($DomainName) on $($ComputerName)" -ComputerName $ComputerName -ScriptBlock { 31 | param ( 32 | $ParentForest, 33 | 34 | $DomainName 35 | ) 36 | $paramInstallADDSDomain = @{ 37 | CreateDnsDelegation = $true 38 | ParentDomainName = $ParentForest 39 | InstallDns = $true 40 | Force = $true 41 | SafeModeAdministratorPassword = ("Test1234" | ConvertTo-SecureString -AsPlainText -Force) 42 | Credential = (New-Object PSCredential("$($ParentForest.Split(".")[0])\Administrator", ("Somepass1" | ConvertTo-SecureString -AsPlainText -Force))) 43 | SkipPreChecks = $true 44 | NewDomainName = $DomainName 45 | } 46 | 47 | Install-ADDSDomain @paramInstallADDSDomain 48 | } -ArgumentList $ParentForest, $DomainName 49 | } 50 | 51 | function Set-LabDnsServer 52 | { 53 | [CmdletBinding()] 54 | param ( 55 | [string[]] 56 | $ClientVMName, 57 | 58 | [string] 59 | $ServerVMName 60 | ) 61 | 62 | $serverIP = (Get-LabVM -ComputerName $ServerVMName).IpV4Address 63 | 64 | Invoke-LabCommand -ActivityName "Configuring DNS Server to: $($ServerVMName) ($($serverIP)) on $($ClientVMName)" -ComputerName $ClientVMName -ScriptBlock { 65 | param ( 66 | $DnsServer 67 | ) 68 | $mainInterface = (Get-NetIPInterface -AddressFamily IPv4 -InterfaceAlias Ethernet).ifIndex 69 | Set-DnsClientServerAddress -InterfaceIndex $mainInterface -ServerAddresses $DnsServer 70 | } -ArgumentList $serverIP 71 | } 72 | 73 | function New-LabADTrust 74 | { 75 | [CmdletBinding()] 76 | param ( 77 | [string[]] 78 | $ComputerName, 79 | 80 | [string] 81 | $RemoteForest, 82 | 83 | [ValidateSet('Bidirectional', 'Outbound', 'Inbound')] 84 | [string] 85 | $Direction = 'Bidirectional' 86 | ) 87 | 88 | Invoke-LabCommand -ActivityName "Creating Forest Trust" -ComputerName $ComputerName -ScriptBlock { 89 | param ( 90 | $RemoteForest, 91 | 92 | $Direction 93 | ) 94 | $remoteContext = New-Object -TypeName "System.DirectoryServices.ActiveDirectory.DirectoryContext" -ArgumentList @("Forest", $RemoteForest, "Administrator", "Somepass1") 95 | $RemoteForest = [System.DirectoryServices.ActiveDirectory.Forest]::getForest($remoteContext) 96 | $localforest = [System.DirectoryServices.ActiveDirectory.Forest]::getCurrentForest() 97 | $localForest.CreateTrustRelationship($RemoteForest, $Direction) 98 | } -ArgumentList $RemoteForest, $Direction 99 | } 100 | 101 | function Add-LabDNSForwarder 102 | { 103 | [CmdletBinding()] 104 | param ( 105 | [string[]] 106 | $ComputerName, 107 | 108 | [string] 109 | $ServerVMName 110 | ) 111 | 112 | $serverIP = (Get-LabVM -ComputerName $ServerVMName).IpV4Address 113 | 114 | Invoke-LabCommand -ActivityName "Adding DNS Forwarder to: $($ServerVMName) ($($serverIP)) on $($ComputerName)" -ComputerName $ComputerName -ScriptBlock { 115 | param ( 116 | $DnsServer 117 | ) 118 | 119 | Add-DnsServerForwarder -IPAddress $DnsServer 120 | } -ArgumentList $serverIP 121 | } 122 | #endregion Utility Functions 123 | 124 | New-LabDefinition -Name $labname -DefaultVirtualizationEngine HyperV 125 | 126 | $parameters = @{ 127 | Memory = 2GB 128 | OperatingSystem = $imageUI 129 | } 130 | 131 | Add-LabMachineDefinition -Name LabGPDCF1 -DomainName $forest1 -Roles RootDC @parameters 132 | Add-LabMachineDefinition -Name LabGPDCF2 -DomainName $forest2 -Roles RootDC @parameters 133 | Add-LabMachineDefinition -Name LabGPDCF1SD @parameters 134 | Add-LabMachineDefinition -Name LabGPDCF2SD @parameters 135 | 136 | Install-Lab 137 | Install-LabWindowsFeature -ComputerName LabGPDCF1SD, LabGPDCF2SD -FeatureName AD-Domain-Services -IncludeManagementTools 138 | 139 | New-LabChildDomain -ComputerName LabGPDCF1SD -DnsServer LabGPDCF1 -ParentForest $forest1 -DomainName $forest1_subdomain.Split(".")[0] 140 | New-LabChildDomain -ComputerName LabGPDCF2SD -DnsServer LabGPDCF2 -ParentForest $forest2 -DomainName $forest2_subdomain.Split(".")[0] 141 | 142 | Restart-LabVM -ComputerName (Get-LabVM) 143 | Start-Sleep -Seconds 120 144 | 145 | Invoke-LabCommand -ActivityName 'Setting Up AD Structure' -ComputerName LabGPDCF1SD, LabGPDCF2SD -ScriptBlock { 146 | $domain = Get-ADDomain 147 | 148 | # OU Structure 149 | $baseOU = New-ADOrganizationalUnit -Path $domain.DistinguishedName -Name Company -PassThru 150 | $serversOU = New-ADOrganizationalUnit -Path $baseOU -Name Servers -PassThru 151 | $usersOU = New-ADOrganizationalUnit -Path $baseOU -Name Users -PassThru 152 | $groupsOU = New-ADOrganizationalUnit -Path $baseOU -Name Groups -PassThru 153 | $clientsOU = New-ADOrganizationalUnit -Path $baseOU -Name Clients -PassThru 154 | $serviceAccountsOU = New-ADOrganizationalUnit -Path $baseOU -Name ServiceAccounts -PassThru 155 | 156 | # User Accounts 157 | $param = @{ 158 | Enabled = $true 159 | PassThru = $true 160 | AccountPassword = "Test1234" | ConvertTo-SecureString -AsPlainText -Force 161 | Path = $usersOU 162 | } 163 | $userMax = New-ADUser @param -Name mm -GivenName Max -Surname Mustermann 164 | $userMaria = New-ADUser @param -Name ma -GivenName Maria -Surname Musterfrau 165 | $userAria = New-ADUser @param -Name am -GivenName Aria -Surname Musterfrau 166 | 167 | # Groups 168 | $param = @{ 169 | PassThru = $true 170 | Path = $groupsOU 171 | GroupScope = 'Global' 172 | } 173 | $groupMaintal = New-ADGroup @param -Name Maintal 174 | $groupLothringen = New-ADGroup @param -Name Lothringen 175 | $groupPreussen = New-ADGroup @param -Name Preussen 176 | 177 | # Group Memberships 178 | Add-ADGroupMember -Identity $groupMaintal -Members $userMax, $userMaria 179 | Add-ADGroupMember -Identity $groupLothringen -Members $userAria 180 | Add-ADGroupMember -Identity $groupPreussen -Members $groupMaintal,$userAria 181 | } 182 | 183 | Invoke-LabCommand -ActivityName "Setting Keyboard Layout" -ComputerName (Get-LabVM).Name -ScriptBlock { Set-WinUserLanguageList -LanguageList 'de-de' -Confirm:$false -Force } 184 | Restart-LabVM -ComputerName (Get-LabVM).Name -------------------------------------------------------------------------------- /library/GPOTools/GPOTools.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2010 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{CFC2CBAA-FD6D-4390-9169-0D9A9198FE81}") = "GPOTools", "GPOTools\GPOTools.csproj", "{0F6CF0D6-A7EF-4CF9-BDEF-39984A57AB31}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {0F6CF0D6-A7EF-4CF9-BDEF-39984A57AB31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {0F6CF0D6-A7EF-4CF9-BDEF-39984A57AB31}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {0F6CF0D6-A7EF-4CF9-BDEF-39984A57AB31}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {0F6CF0D6-A7EF-4CF9-BDEF-39984A57AB31}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {B993DDB7-5DC7-4E62-8642-A2CB535DE64C} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /library/GPOTools/GPOTools/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GPOTools 4 | { 5 | public class Class1 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /library/GPOTools/GPOTools/GPOTools.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net4.5.2 5 | 6 | 7 | 8 | ..\..\..\GPOTools\bin 9 | ..\..\..\GPOTools\bin\GPOTools.xml 10 | 11 | 12 | 13 | ..\..\..\GPOTools\bin 14 | ..\..\..\GPOTools\bin\GPOTools.xml 15 | 16 | 17 | 18 | false 19 | 20 | 21 | 22 | --------------------------------------------------------------------------------