# GPOCore.ps1 # Shared utility functions used by multiple GPO modules. # Must be loaded before other GPO modules (they depend on these functions). function Get-GPOSysvolPath { <# .SYNOPSIS Resolves a GPO name to its SYSVOL filesystem path. #> param( [Parameter(Mandatory)] [string]$GPOName, [string]$Domain = (Get-ADDomain).DNSRoot ) $gpo = Get-GPO -Name $GPOName -ErrorAction Stop $guid = "{$($gpo.Id)}" return "\\$Domain\SYSVOL\$Domain\Policies\$guid" } function Get-GPOSecurityTemplatePath { <# .SYNOPSIS Returns the full path to a GPO's GptTmpl.inf file. #> param( [Parameter(Mandatory)] [string]$GPOName, [string]$Domain = (Get-ADDomain).DNSRoot ) $sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain return Join-Path $sysvolPath 'MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf' } function Update-GPOVersion { <# .SYNOPSIS Bumps a GPO's version number in both AD and GPT.INI. .DESCRIPTION The GPO version number is a packed 32-bit integer: - Upper 16 bits = user configuration version - Lower 16 bits = machine configuration version This function increments only the relevant half based on -Scope. #> param( [Parameter(Mandatory)] [string]$GPOName, [ValidateSet('Machine', 'User', 'Both')] [string]$Scope = 'Machine', [string]$Domain = (Get-ADDomain).DNSRoot ) $gpo = Get-GPO -Name $GPOName -Domain $Domain $gpoGuid = "{$($gpo.Id)}" $gpoDN = "CN=$gpoGuid,CN=Policies,CN=System,$((Get-ADDomain -Server $Domain).DistinguishedName)" # Read current packed version and split into halves $adVersion = [int](Get-ADObject $gpoDN -Properties versionNumber).versionNumber $machineVer = $adVersion -band 0xFFFF $userVer = ($adVersion -shr 16) -band 0xFFFF switch ($Scope) { 'Machine' { $machineVer++ } 'User' { $userVer++ } 'Both' { $machineVer++; $userVer++ } } $newVersion = ($userVer -shl 16) -bor $machineVer Set-ADObject $gpoDN -Replace @{ versionNumber = $newVersion } # GPT.INI uses the same packed format $gptIniPath = Join-Path (Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain) 'GPT.INI' if (Test-Path $gptIniPath) { $gptContent = Get-Content $gptIniPath -Raw if ($gptContent -match 'Version=(\d+)') { $oldVer = [int]$Matches[1] $oldMachine = $oldVer -band 0xFFFF $oldUser = ($oldVer -shr 16) -band 0xFFFF switch ($Scope) { 'Machine' { $oldMachine++ } 'User' { $oldUser++ } 'Both' { $oldMachine++; $oldUser++ } } $newGptVer = ($oldUser -shl 16) -bor $oldMachine $gptContent = $gptContent -replace "Version=$oldVer", "Version=$newGptVer" Set-Content -Path $gptIniPath -Value $gptContent -NoNewline } } } function Add-GPOExtensionGuids { <# .SYNOPSIS Ensures the specified CSE/tool GUID pair is present in a GPO's extension name attributes (gPCMachineExtensionNames or gPCUserExtensionNames). #> param( [Parameter(Mandatory)] [string]$GPOName, [Parameter(Mandatory)] [string]$CseGuid, [Parameter(Mandatory)] [string]$ToolGuid, [ValidateSet('Machine', 'User')] [string]$Scope = 'Machine', [string]$Domain = (Get-ADDomain).DNSRoot ) $gpo = Get-GPO -Name $GPOName -Domain $Domain $gpoGuid = "{$($gpo.Id)}" $gpoDN = "CN=$gpoGuid,CN=Policies,CN=System,$((Get-ADDomain -Server $Domain).DistinguishedName)" $attrName = if ($Scope -eq 'Machine') { 'gPCMachineExtensionNames' } else { 'gPCUserExtensionNames' } $obj = Get-ADObject $gpoDN -Properties $attrName $current = $obj.$attrName if (-not $current) { $current = '' } $pair = "[$CseGuid$ToolGuid]" if ($current.Contains($pair)) { return } # Parse existing pairs, add new one, sort by CSE GUID $pairs = [System.Collections.Generic.List[string]]::new() $regex = [regex]'\[\{[0-9A-Fa-f-]+\}\{[0-9A-Fa-f-]+\}\]' foreach ($m in $regex.Matches($current)) { $pairs.Add($m.Value) } $pairs.Add($pair) $sorted = $pairs | Sort-Object $newValue = -join $sorted Set-ADObject $gpoDN -Replace @{ $attrName = $newValue } } function Compare-GPOStatus { <# .SYNOPSIS Compares the desired GPO status (enabled/disabled sections) against the current state. Returns a diff object if they differ. #> param( [Parameter(Mandatory)] [string]$GPOName, [bool]$DisableUserConfiguration = $false, [bool]$DisableComputerConfiguration = $false ) $gpo = Get-GPO -Name $GPOName -ErrorAction Stop $desiredStatus = 'AllSettingsEnabled' if ($DisableUserConfiguration -and $DisableComputerConfiguration) { $desiredStatus = 'AllSettingsDisabled' } elseif ($DisableUserConfiguration) { $desiredStatus = 'UserSettingsDisabled' } elseif ($DisableComputerConfiguration) { $desiredStatus = 'ComputerSettingsDisabled' } $currentStatus = $gpo.GpoStatus.ToString() if ($currentStatus -ne $desiredStatus) { Write-Host " [DRIFT] GpoStatus: '$currentStatus' -> '$desiredStatus'" -ForegroundColor Red return [PSCustomObject]@{ Type = 'GpoStatus' GPO = $GPOName Current = $currentStatus Desired = $desiredStatus } } Write-Host " [OK] GpoStatus: $currentStatus" -ForegroundColor Green return $null } function Ensure-GPOStatus { <# .SYNOPSIS Sets the GPO status (enabled/disabled Computer/User configuration). Uses the GPO object's GpoStatus property which sets gPCOptions internally. #> param( [Parameter(Mandatory)] [string]$GPOName, [bool]$DisableUserConfiguration = $false, [bool]$DisableComputerConfiguration = $false ) $gpo = Get-GPO -Name $GPOName -ErrorAction Stop $desiredStatus = 'AllSettingsEnabled' if ($DisableUserConfiguration -and $DisableComputerConfiguration) { $desiredStatus = 'AllSettingsDisabled' } elseif ($DisableUserConfiguration) { $desiredStatus = 'UserSettingsDisabled' } elseif ($DisableComputerConfiguration) { $desiredStatus = 'ComputerSettingsDisabled' } $currentStatus = $gpo.GpoStatus.ToString() if ($currentStatus -ne $desiredStatus) { $gpo.GpoStatus = [Microsoft.GroupPolicy.GpoStatus]::$desiredStatus Write-Host " [UPDATED] GpoStatus: $currentStatus -> $desiredStatus" -ForegroundColor Yellow } } # ------------------------------------------------------------------- # DSC Helper Functions # ------------------------------------------------------------------- function Resolve-SIDsToNames { <# .SYNOPSIS Translates a GptTmpl.inf SID string into an array of NTAccount names. .EXAMPLE Resolve-SIDsToNames '*S-1-5-32-544,*S-1-5-20' # Returns: @('BUILTIN\Administrators', 'NT AUTHORITY\NETWORK SERVICE') #> param( [Parameter(Mandatory)] [string]$SIDString ) $sids = $SIDString -split ',' | ForEach-Object { $_.Trim().TrimStart('*') } $names = @() foreach ($sidStr in $sids) { try { $sid = New-Object System.Security.Principal.SecurityIdentifier($sidStr) $account = $sid.Translate([System.Security.Principal.NTAccount]) $names += $account.Value } catch { # If translation fails (e.g., virtual service account), keep the raw SID Write-Warning "Could not resolve SID '$sidStr' to a name. Using raw SID." $names += $sidStr } } return $names } function Get-GptTmplRegValue { <# .SYNOPSIS Parses a GptTmpl.inf Registry Values entry and returns the numeric value. .DESCRIPTION GptTmpl.inf stores registry values as 'type,value' (e.g., '4,1' = REG_DWORD 1). This function strips the type prefix and returns the value as an integer. .EXAMPLE Get-GptTmplRegValue '4,1' # Returns: 1 #> param( [Parameter(Mandatory)] [string]$RegValueString ) $parts = $RegValueString -split ',', 2 if ($parts.Count -lt 2) { throw "Invalid GptTmpl.inf registry value format: '$RegValueString'. Expected 'type,value'." } return [int]$parts[1] }