Infrastructure-as-code framework for Active Directory objects and Group Policy. Sanitized from production deployment for public sharing.
378 lines
12 KiB
PowerShell
378 lines
12 KiB
PowerShell
# GPOPolicy.ps1
|
|
# GptTmpl.inf handling and registry-based Administrative Template settings.
|
|
# Depends on: GPOCore.ps1 (Get-GPOSysvolPath, Get-GPOSecurityTemplatePath)
|
|
|
|
function Read-GptTmplInf {
|
|
<#
|
|
.SYNOPSIS
|
|
Parses a GptTmpl.inf file into a nested hashtable keyed by section name.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$Path
|
|
)
|
|
|
|
if (-not (Test-Path $Path)) {
|
|
return @{}
|
|
}
|
|
|
|
$result = @{}
|
|
$currentSection = $null
|
|
|
|
# GptTmpl.inf is UTF-16LE
|
|
$lines = [System.IO.File]::ReadAllLines($Path, [System.Text.Encoding]::Unicode)
|
|
|
|
foreach ($line in $lines) {
|
|
$line = $line.Trim()
|
|
if ($line -match '^\[(.+)\]$') {
|
|
$currentSection = $Matches[1]
|
|
if (-not $result.Contains($currentSection)) {
|
|
$result[$currentSection] = [ordered]@{}
|
|
}
|
|
}
|
|
elseif ($line -match '^(.+?)\s*=\s*(.*)$' -and $currentSection) {
|
|
$result[$currentSection][$Matches[1].Trim()] = $Matches[2].Trim()
|
|
}
|
|
}
|
|
|
|
return $result
|
|
}
|
|
|
|
function ConvertTo-GptTmplInf {
|
|
<#
|
|
.SYNOPSIS
|
|
Converts a settings hashtable into GptTmpl.inf content string.
|
|
.DESCRIPTION
|
|
Takes the SecurityPolicy hashtable from a settings.ps1 file and produces
|
|
the INI-format content for a GptTmpl.inf file.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[hashtable]$Settings
|
|
)
|
|
|
|
$sb = [System.Text.StringBuilder]::new()
|
|
[void]$sb.AppendLine('[Unicode]')
|
|
[void]$sb.AppendLine('Unicode=yes')
|
|
|
|
# Ordered section output
|
|
$sectionOrder = @(
|
|
'System Access'
|
|
'Kerberos Policy'
|
|
'Event Audit'
|
|
'Registry Values'
|
|
'Privilege Rights'
|
|
'Group Membership'
|
|
)
|
|
|
|
foreach ($section in $sectionOrder) {
|
|
if ($Settings.Contains($section)) {
|
|
[void]$sb.AppendLine("[$section]")
|
|
foreach ($key in $Settings[$section].Keys) {
|
|
[void]$sb.AppendLine("$key = $($Settings[$section][$key])")
|
|
}
|
|
}
|
|
}
|
|
|
|
# Include any sections not in the predefined order
|
|
foreach ($section in $Settings.Keys) {
|
|
if ($section -notin $sectionOrder -and $section -ne 'Unicode') {
|
|
[void]$sb.AppendLine("[$section]")
|
|
foreach ($key in $Settings[$section].Keys) {
|
|
[void]$sb.AppendLine("$key = $($Settings[$section][$key])")
|
|
}
|
|
}
|
|
}
|
|
|
|
[void]$sb.AppendLine('[Version]')
|
|
[void]$sb.AppendLine('signature="$CHICAGO$"')
|
|
[void]$sb.AppendLine('Revision=1')
|
|
|
|
return $sb.ToString()
|
|
}
|
|
|
|
function Set-GPOSecurityPolicy {
|
|
<#
|
|
.SYNOPSIS
|
|
Writes security policy settings to a GPO's GptTmpl.inf in SYSVOL
|
|
and bumps the GPO version number in AD so clients pick up the change.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$GPOName,
|
|
|
|
[Parameter(Mandatory)]
|
|
[hashtable]$SecurityPolicy,
|
|
|
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
|
)
|
|
|
|
$infPath = Get-GPOSecurityTemplatePath -GPOName $GPOName -Domain $Domain
|
|
$infDir = Split-Path $infPath -Parent
|
|
|
|
# Ensure directory structure exists
|
|
if (-not (Test-Path $infDir)) {
|
|
New-Item -ItemType Directory -Path $infDir -Force | Out-Null
|
|
}
|
|
|
|
# Generate and write the inf content as UTF-16LE (required by Windows)
|
|
$content = ConvertTo-GptTmplInf -Settings $SecurityPolicy
|
|
[System.IO.File]::WriteAllText($infPath, $content, [System.Text.Encoding]::Unicode)
|
|
|
|
Write-Host " Written: $infPath" -ForegroundColor Green
|
|
}
|
|
|
|
function Compare-GPOSecurityPolicy {
|
|
<#
|
|
.SYNOPSIS
|
|
Compares desired security policy settings against what's currently
|
|
in the GPO's GptTmpl.inf. Returns differences.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$GPOName,
|
|
|
|
[Parameter(Mandatory)]
|
|
[hashtable]$SecurityPolicy,
|
|
|
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
|
)
|
|
|
|
$infPath = Get-GPOSecurityTemplatePath -GPOName $GPOName -Domain $Domain
|
|
$current = Read-GptTmplInf -Path $infPath
|
|
$differences = @()
|
|
|
|
foreach ($section in $SecurityPolicy.Keys) {
|
|
foreach ($key in $SecurityPolicy[$section].Keys) {
|
|
$desired = [string]$SecurityPolicy[$section][$key]
|
|
$actual = $null
|
|
|
|
if ($current.Contains($section) -and $current[$section].Contains($key)) {
|
|
$actual = [string]$current[$section][$key]
|
|
}
|
|
|
|
if ($actual -ne $desired) {
|
|
$differences += [PSCustomObject]@{
|
|
Section = $section
|
|
Setting = $key
|
|
Current = if ($null -eq $actual) { '(not set)' } else { $actual }
|
|
Desired = $desired
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $differences
|
|
}
|
|
|
|
# -------------------------------------------------------------------
|
|
# Restricted Groups ([Group Membership] section in GptTmpl.inf)
|
|
# -------------------------------------------------------------------
|
|
|
|
function ConvertTo-RestrictedGroupEntries {
|
|
<#
|
|
.SYNOPSIS
|
|
Converts a friendly RestrictedGroups hashtable into [Group Membership]
|
|
key-value pairs for GptTmpl.inf.
|
|
.DESCRIPTION
|
|
GptTmpl.inf [Group Membership] uses SID-based keys:
|
|
*S-1-5-32-544__Members = *S-1-5-21-xxx,*S-1-5-21-yyy
|
|
*S-1-5-32-544__Memberof =
|
|
This function resolves group/account names to SIDs automatically.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[hashtable]$RestrictedGroups
|
|
)
|
|
|
|
$entries = [ordered]@{}
|
|
|
|
foreach ($groupName in $RestrictedGroups.Keys) {
|
|
$groupDef = $RestrictedGroups[$groupName]
|
|
|
|
# Resolve target group to SID
|
|
try {
|
|
$ntAccount = New-Object System.Security.Principal.NTAccount($groupName)
|
|
$groupSid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
|
} catch {
|
|
throw "RestrictedGroups: cannot resolve group '$groupName' to a SID: $_"
|
|
}
|
|
|
|
# Members
|
|
$memberSids = @()
|
|
if ($groupDef.Members) {
|
|
foreach ($member in $groupDef.Members) {
|
|
try {
|
|
$memberNt = New-Object System.Security.Principal.NTAccount($member)
|
|
$memberSid = $memberNt.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
|
$memberSids += "*$memberSid"
|
|
} catch {
|
|
throw "RestrictedGroups: cannot resolve member '$member' of group '$groupName' to a SID: $_"
|
|
}
|
|
}
|
|
}
|
|
$entries["*${groupSid}__Members"] = $memberSids -join ','
|
|
|
|
# Memberof
|
|
$memberofSids = @()
|
|
if ($groupDef.Memberof) {
|
|
foreach ($parent in $groupDef.Memberof) {
|
|
try {
|
|
$parentNt = New-Object System.Security.Principal.NTAccount($parent)
|
|
$parentSid = $parentNt.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
|
$memberofSids += "*$parentSid"
|
|
} catch {
|
|
throw "RestrictedGroups: cannot resolve parent group '$parent' of '$groupName' to a SID: $_"
|
|
}
|
|
}
|
|
}
|
|
$entries["*${groupSid}__Memberof"] = $memberofSids -join ','
|
|
}
|
|
|
|
return $entries
|
|
}
|
|
|
|
# -------------------------------------------------------------------
|
|
# Registry-Based Settings (Administrative Templates)
|
|
# -------------------------------------------------------------------
|
|
|
|
function Compare-GPORegistrySettings {
|
|
<#
|
|
.SYNOPSIS
|
|
Compares desired registry-based (Administrative Template) settings
|
|
against the current values in a GPO. Returns differences.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$GPOName,
|
|
|
|
[Parameter(Mandatory)]
|
|
[array]$RegistrySettings,
|
|
|
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
|
)
|
|
|
|
$differences = @()
|
|
|
|
foreach ($reg in $RegistrySettings) {
|
|
$desiredDisplay = "$($reg.Value) ($($reg.Type))"
|
|
|
|
try {
|
|
$current = Get-GPRegistryValue `
|
|
-Name $GPOName `
|
|
-Domain $Domain `
|
|
-Key $reg.Key `
|
|
-ValueName $reg.ValueName `
|
|
-ErrorAction Stop
|
|
|
|
$actualValue = $current.Value
|
|
$actualType = $current.Type.ToString()
|
|
|
|
if ([string]$actualValue -ne [string]$reg.Value -or $actualType -ne $reg.Type) {
|
|
$differences += [PSCustomObject]@{
|
|
Key = $reg.Key
|
|
ValueName = $reg.ValueName
|
|
Current = "$actualValue ($actualType)"
|
|
Desired = $desiredDisplay
|
|
}
|
|
}
|
|
} catch {
|
|
# Setting doesn't exist in the GPO yet
|
|
$differences += [PSCustomObject]@{
|
|
Key = $reg.Key
|
|
ValueName = $reg.ValueName
|
|
Current = '(not set)'
|
|
Desired = $desiredDisplay
|
|
}
|
|
}
|
|
}
|
|
|
|
# --- Stale value detection ---
|
|
# For each unique key in settings, check if the GPO has values not in the declared set
|
|
$uniqueKeys = $RegistrySettings | ForEach-Object { $_.Key } | Select-Object -Unique
|
|
$declaredLookup = @{}
|
|
foreach ($reg in $RegistrySettings) {
|
|
$declaredLookup["$($reg.Key)|$($reg.ValueName)"] = $true
|
|
}
|
|
|
|
foreach ($key in $uniqueKeys) {
|
|
try {
|
|
$currentValues = Get-GPRegistryValue -Name $GPOName -Domain $Domain `
|
|
-Key $key -ErrorAction Stop
|
|
} catch { continue }
|
|
|
|
foreach ($val in $currentValues) {
|
|
if (-not $val.ValueName) { continue } # skip subkey entries
|
|
$lookupKey = "$key|$($val.ValueName)"
|
|
if (-not $declaredLookup.ContainsKey($lookupKey)) {
|
|
$differences += [PSCustomObject]@{
|
|
Key = $key
|
|
ValueName = $val.ValueName
|
|
Current = "$($val.Value) ($($val.Type))"
|
|
Desired = '(stale -- should be removed)'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $differences
|
|
}
|
|
|
|
function Set-GPORegistrySettings {
|
|
<#
|
|
.SYNOPSIS
|
|
Applies registry-based (Administrative Template) settings to a GPO
|
|
using Set-GPRegistryValue. With -Cleanup, removes stale values under
|
|
managed keys that are not in the declared settings.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$GPOName,
|
|
|
|
[Parameter(Mandatory)]
|
|
[array]$RegistrySettings,
|
|
|
|
[switch]$Cleanup,
|
|
|
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
|
)
|
|
|
|
foreach ($reg in $RegistrySettings) {
|
|
Set-GPRegistryValue `
|
|
-Name $GPOName `
|
|
-Domain $Domain `
|
|
-Key $reg.Key `
|
|
-ValueName $reg.ValueName `
|
|
-Type $reg.Type `
|
|
-Value $reg.Value | Out-Null
|
|
|
|
Write-Host " Set: $($reg.Key)\$($reg.ValueName) = $($reg.Value)" -ForegroundColor Green
|
|
}
|
|
|
|
# Remove stale values under managed keys
|
|
if ($Cleanup) {
|
|
$declaredLookup = @{}
|
|
foreach ($reg in $RegistrySettings) {
|
|
$declaredLookup["$($reg.Key)|$($reg.ValueName)"] = $true
|
|
}
|
|
$uniqueKeys = $RegistrySettings | ForEach-Object { $_.Key } | Select-Object -Unique
|
|
|
|
foreach ($key in $uniqueKeys) {
|
|
try {
|
|
$currentValues = Get-GPRegistryValue -Name $GPOName -Domain $Domain `
|
|
-Key $key -ErrorAction Stop
|
|
} catch { continue }
|
|
|
|
foreach ($val in $currentValues) {
|
|
if (-not $val.ValueName) { continue }
|
|
$lookupKey = "$key|$($val.ValueName)"
|
|
if (-not $declaredLookup.ContainsKey($lookupKey)) {
|
|
Remove-GPRegistryValue -Name $GPOName -Domain $Domain `
|
|
-Key $key -ValueName $val.ValueName | Out-Null
|
|
Write-Host " [REMOVED] Stale: $key\$($val.ValueName)" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|