Infrastructure-as-code framework for Active Directory objects and Group Policy. Sanitized from production deployment for public sharing.
274 lines
8.4 KiB
PowerShell
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]
|
|
}
|