Damien Coles f172d00514 Initial release: Declarative AD Framework v2.1.0
Infrastructure-as-code framework for Active Directory objects and Group Policy.
Sanitized from production deployment for public sharing.
2026-02-19 17:02:42 +00:00

274 lines
8.4 KiB
PowerShell

# 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]
}