# GPOPermissions.ps1 # GPO links, management permissions, and security filtering. # No dependencies on GPOCore.ps1 (uses only AD/GP cmdlets directly). function Normalize-GPOLinkTo { <# .SYNOPSIS Normalizes the LinkTo value from settings.ps1 into a consistent array of hashtables with Target, Order, and Enforced keys. .DESCRIPTION Supports three input formats: - String: 'OU=...' (backward compatible, no order/enforcement management) - Hashtable: @{ Target = 'OU=...'; Order = 1; Enforced = $true } - Array: @( @{ Target = ... }, @{ Target = ... } ) #> param( [Parameter(Mandatory)] $LinkTo ) # Single string if ($LinkTo -is [string]) { return @(@{ Target = $LinkTo; Order = $null; Enforced = $false }) } # Single hashtable with Target key if ($LinkTo -is [hashtable] -and $LinkTo.Target) { return @(@{ Target = $LinkTo.Target Order = if ($LinkTo.ContainsKey('Order')) { $LinkTo.Order } else { $null } Enforced = if ($LinkTo.ContainsKey('Enforced')) { $LinkTo.Enforced } else { $false } }) } # Array of hashtables or strings if ($LinkTo -is [array]) { $result = @() foreach ($item in $LinkTo) { if ($item -is [string]) { $result += @{ Target = $item; Order = $null; Enforced = $false } } elseif ($item -is [hashtable] -and $item.Target) { $result += @{ Target = $item.Target Order = if ($item.ContainsKey('Order')) { $item.Order } else { $null } Enforced = if ($item.ContainsKey('Enforced')) { $item.Enforced } else { $false } } } } return $result } return @() } function Ensure-GPOLink { <# .SYNOPSIS Idempotently links a GPO to an OU. Optionally manages link order and enforcement state. #> param( [Parameter(Mandatory)] [string]$GPOName, [Parameter(Mandatory)] [string]$TargetOU, $Order = $null, [bool]$Enforced = $false, [string]$Domain = (Get-ADDomain).DNSRoot ) $inheritance = Get-GPInheritance -Target $TargetOU $linked = $inheritance.GpoLinks | Where-Object { $_.DisplayName -eq $GPOName } if ($linked) { $changed = $false # Check enforcement $currentEnforced = $linked.Enforced -eq 'Yes' if ($currentEnforced -ne $Enforced) { $enforcedStr = if ($Enforced) { 'Yes' } else { 'No' } Set-GPLink -Name $GPOName -Target $TargetOU -Domain $Domain -Enforced $enforcedStr | Out-Null Write-Host " [UPDATED] Enforced: $currentEnforced -> $Enforced on $TargetOU" -ForegroundColor Yellow $changed = $true } # Check order (only if specified) if ($null -ne $Order -and $linked.Order -ne $Order) { Set-GPLink -Name $GPOName -Target $TargetOU -Domain $Domain -Order $Order | Out-Null Write-Host " [UPDATED] Link order: $($linked.Order) -> $Order on $TargetOU" -ForegroundColor Yellow $changed = $true } if (-not $changed) { Write-Host " Already linked: $GPOName -> $TargetOU" -ForegroundColor Green } } else { $params = @{ Name = $GPOName Target = $TargetOU Domain = $Domain } if ($null -ne $Order) { $params.Order = $Order } if ($Enforced) { $params.Enforced = 'Yes' } New-GPLink @params | Out-Null Write-Host " Linked: $GPOName -> $TargetOU" -ForegroundColor Yellow } } function Compare-GPOLink { <# .SYNOPSIS Checks whether a GPO is linked to the specified OU with correct order and enforcement. Returns diff objects for any discrepancies. #> param( [Parameter(Mandatory)] [string]$GPOName, [Parameter(Mandatory)] [string]$TargetOU, $Order = $null, [bool]$Enforced = $false ) $diffs = @() $inheritance = Get-GPInheritance -Target $TargetOU $linked = $inheritance.GpoLinks | Where-Object { $_.DisplayName -eq $GPOName } if (-not $linked) { Write-Host " [DRIFT] Not linked: $GPOName -> $TargetOU" -ForegroundColor Red $diffs += [PSCustomObject]@{ Type = 'GPOLink' GPO = $GPOName TargetOU = $TargetOU Status = 'Not linked' } return $diffs } Write-Host " [OK] Linked: $GPOName -> $TargetOU" -ForegroundColor Green # Check enforcement $currentEnforced = $linked.Enforced -eq 'Yes' if ($currentEnforced -ne $Enforced) { Write-Host " [DRIFT] Enforced: $currentEnforced -> $Enforced on $TargetOU" -ForegroundColor Red $diffs += [PSCustomObject]@{ Type = 'GPOLinkEnforced' GPO = $GPOName TargetOU = $TargetOU Current = $currentEnforced Desired = $Enforced } } # Check order (only if specified) if ($null -ne $Order -and $linked.Order -ne $Order) { Write-Host " [DRIFT] Link order: $($linked.Order) -> $Order on $TargetOU" -ForegroundColor Red $diffs += [PSCustomObject]@{ Type = 'GPOLinkOrder' GPO = $GPOName TargetOU = $TargetOU Current = $linked.Order Desired = $Order } } return $diffs } # ------------------------------------------------------------------- # Security Filtering (Deny Apply Group Policy) # ------------------------------------------------------------------- function Ensure-GPOSecurityFiltering { <# .SYNOPSIS Adds Deny Apply Group Policy ACEs to a GPO for specified groups. Uses the AD ACL on the GPO object -- Set-GPPermission doesn't support Deny. #> param( [Parameter(Mandatory)] [string]$GPOName, [string[]]$DenyApply = @() ) if ($DenyApply.Count -eq 0) { return } $gpo = Get-GPO -Name $GPOName -ErrorAction Stop $domainDN = (Get-ADDomain).DistinguishedName $gpoADPath = "AD:CN={$($gpo.Id)},CN=Policies,CN=System,$domainDN" $acl = Get-Acl $gpoADPath # Apply Group Policy extended right GUID $applyGpoGuid = [Guid]'edacfd8f-ffb3-11d1-b41d-00a0c968f939' foreach ($groupName in $DenyApply) { $group = Get-ADGroup -Identity $groupName -ErrorAction Stop $sid = New-Object System.Security.Principal.SecurityIdentifier($group.SID) # Check if a Deny rule already exists for this SID + extended right $alreadyDenied = $acl.Access | Where-Object { $_.AccessControlType -eq 'Deny' -and $_.ObjectType -eq $applyGpoGuid -and $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -eq $sid.Value } if ($alreadyDenied) { Write-Host " [OK] $groupName already denied Apply on GPO: $GPOName" -ForegroundColor Green } else { $denyRule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule( $sid, [System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight, [System.Security.AccessControl.AccessControlType]::Deny, $applyGpoGuid ) $acl.AddAccessRule($denyRule) Set-Acl $gpoADPath $acl Write-Host " [DENY] $groupName denied Apply on GPO: $GPOName" -ForegroundColor Yellow } } } function Compare-GPOSecurityFiltering { <# .SYNOPSIS Checks whether Deny Apply Group Policy ACEs exist for the specified groups. Returns diffs for any missing deny rules. #> param( [Parameter(Mandatory)] [string]$GPOName, [string[]]$DenyApply = @() ) $diffs = @() if ($DenyApply.Count -eq 0) { return $diffs } $gpo = Get-GPO -Name $GPOName -ErrorAction Stop $domainDN = (Get-ADDomain).DistinguishedName $gpoADPath = "AD:CN={$($gpo.Id)},CN=Policies,CN=System,$domainDN" $acl = Get-Acl $gpoADPath $applyGpoGuid = [Guid]'edacfd8f-ffb3-11d1-b41d-00a0c968f939' foreach ($groupName in $DenyApply) { $group = Get-ADGroup -Identity $groupName -ErrorAction Stop $sid = New-Object System.Security.Principal.SecurityIdentifier($group.SID) $found = $acl.Access | Where-Object { $_.AccessControlType -eq 'Deny' -and $_.ObjectType -eq $applyGpoGuid -and $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -eq $sid.Value } if ($found) { Write-Host " [OK] $groupName denied Apply on GPO: $GPOName" -ForegroundColor Green } else { Write-Host " [DRIFT] $groupName not denied Apply on GPO: $GPOName" -ForegroundColor Red $diffs += [PSCustomObject]@{ Type = 'SecurityFiltering' GPO = $GPOName Group = $groupName Status = 'Missing Deny' } } } return $diffs } # ------------------------------------------------------------------- # GPO Management Permissions # ------------------------------------------------------------------- function Ensure-GPOManagementPermission { <# .SYNOPSIS Ensures a security group has GpoEditDeleteModifySecurity on a GPO. Idempotent: no-op if the permission already exists. #> param( [Parameter(Mandatory)] [string]$GPOName, [Parameter(Mandatory)] [string]$GroupName ) $hasPermission = $false try { $perm = Get-GPPermission -Name $GPOName -TargetName $GroupName -TargetType Group -ErrorAction Stop if ($perm.Permission -eq 'GpoEditDeleteModifySecurity') { $hasPermission = $true } } catch { # Group has no permissions on this GPO } if ($hasPermission) { Write-Host " [OK] $GroupName has edit rights on GPO: $GPOName" -ForegroundColor Green } else { Set-GPPermission -Name $GPOName -PermissionLevel GpoEditDeleteModifySecurity ` -TargetName $GroupName -TargetType Group -Replace | Out-Null Write-Host " [GRANTED] $GroupName edit rights on GPO: $GPOName" -ForegroundColor Yellow } } function Compare-GPOManagementPermission { <# .SYNOPSIS Checks whether a security group has GpoEditDeleteModifySecurity on a GPO. Returns a diff object if the permission is missing. #> param( [Parameter(Mandatory)] [string]$GPOName, [Parameter(Mandatory)] [string]$GroupName ) $hasPermission = $false try { $perm = Get-GPPermission -Name $GPOName -TargetName $GroupName -TargetType Group -ErrorAction Stop if ($perm.Permission -eq 'GpoEditDeleteModifySecurity') { $hasPermission = $true } } catch { # Group has no permissions on this GPO } if ($hasPermission) { Write-Host " [OK] $GroupName has edit rights on GPO: $GPOName" -ForegroundColor Green } else { Write-Host " [DRIFT] $GroupName missing edit rights on GPO: $GPOName" -ForegroundColor Red return [PSCustomObject]@{ Type = 'ManagementPermission' GPO = $GPOName Group = $GroupName Status = 'Missing GpoEditDeleteModifySecurity' } } }