Infrastructure-as-code framework for Active Directory objects and Group Policy. Sanitized from production deployment for public sharing.
838 lines
32 KiB
PowerShell
838 lines
32 KiB
PowerShell
# GPOPreferences.ps1
|
|
# Group Policy Preferences XML generation for 10 types.
|
|
# Depends on: GPOCore.ps1 (Get-GPOSysvolPath, Add-GPOExtensionGuids)
|
|
|
|
$Script:GppToolGuid = '{CAB54552-DEEA-4691-817E-ED4A4D1AFC72}'
|
|
|
|
$Script:GppActionCode = @{ Create = 'C'; Replace = 'R'; Update = 'U'; Delete = 'D' }
|
|
$Script:GppActionImage = @{ Create = '0'; Replace = '1'; Update = '2'; Delete = '3' }
|
|
|
|
$Script:GppTypeInfo = @{
|
|
ScheduledTasks = @{
|
|
RootClsid = '{CC63F200-7309-4ba0-B154-A71CD118DBCC}'
|
|
ItemClsid = '{D8896631-B747-47a7-84A6-C155337F3BC8}'
|
|
SysvolDir = 'ScheduledTasks'
|
|
FileName = 'ScheduledTasks.xml'
|
|
CseGuid = '{AADCED64-746C-4633-A97C-D61349046527}'
|
|
NameKey = 'Name'
|
|
}
|
|
DriveMaps = @{
|
|
RootClsid = '{8FDDCC1A-0C3C-43cd-A6B4-71A6DF20DA8C}'
|
|
ItemClsid = '{935D1B74-9CB8-4e3c-9914-7DD559B7A417}'
|
|
SysvolDir = 'Drives'
|
|
FileName = 'Drives.xml'
|
|
CseGuid = '{5794DAFD-BE60-433f-88A2-1A31939AC01F}'
|
|
NameKey = 'Letter'
|
|
}
|
|
EnvironmentVariables = @{
|
|
RootClsid = '{D76B9641-3288-4f75-942D-087DE603E3EA}'
|
|
ItemClsid = '{B1A3FA3F-E5D1-41ea-88D1-55CE96B6C78B}'
|
|
SysvolDir = 'EnvironmentVariables'
|
|
FileName = 'EnvironmentVariables.xml'
|
|
CseGuid = '{0E28E245-9368-4853-AD84-6DA3BA35BB75}'
|
|
NameKey = 'Name'
|
|
}
|
|
Services = @{
|
|
RootClsid = '{2CFB484A-4E86-4eb1-8B6A-E1535488BDBF}'
|
|
ItemClsid = '{AB6F0B67-D4E1-4f59-A9E5-F6BD3A72F4FF}'
|
|
SysvolDir = 'Services'
|
|
FileName = 'Services.xml'
|
|
CseGuid = '{91FBB303-0CD5-4055-BF42-E512A681B325}'
|
|
NameKey = 'ServiceName'
|
|
}
|
|
Printers = @{
|
|
RootClsid = '{1F577D12-3D1B-471e-A1B7-060317597B9C}'
|
|
ItemClsid = '{9A5E9697-9095-436d-A0EE-4D128FDFBCE5}'
|
|
SysvolDir = 'Printers'
|
|
FileName = 'Printers.xml'
|
|
CseGuid = '{A8C42CEA-CDB8-4388-97F4-5831F933DA84}'
|
|
NameKey = 'Path'
|
|
}
|
|
Shortcuts = @{
|
|
RootClsid = '{872ECB34-B2EC-401b-A585-D32574AA90EE}'
|
|
ItemClsid = '{4F2F7C55-2790-433e-8127-0739D1CFA327}'
|
|
SysvolDir = 'Shortcuts'
|
|
FileName = 'Shortcuts.xml'
|
|
CseGuid = '{C418DD9D-0D14-4EFB-8FBF-CFE535C8FAC7}'
|
|
NameKey = 'Name'
|
|
}
|
|
Files = @{
|
|
RootClsid = '{215B2E53-57CE-475c-80FE-9EEC14635851}'
|
|
ItemClsid = '{50BE44C8-567A-4ed1-B1D0-9234FE1F38AF}'
|
|
SysvolDir = 'Files'
|
|
FileName = 'Files.xml'
|
|
CseGuid = '{7150F9BF-48AD-4DA4-A49C-29EF4A8369BA}'
|
|
NameKey = 'TargetPath'
|
|
}
|
|
NetworkShares = @{
|
|
RootClsid = '{520870D8-A6E7-47e8-A8D8-E6A4E76EAEC2}'
|
|
ItemClsid = '{2888C5E7-94FC-4739-90AA-2C1536D68BC0}'
|
|
SysvolDir = 'NetworkShares'
|
|
FileName = 'NetworkShares.xml'
|
|
CseGuid = '{6A4C88C6-C502-4F74-8F60-2CB23EDC24E2}'
|
|
NameKey = 'Name'
|
|
}
|
|
RegistryItems = @{
|
|
RootClsid = '{A3CCFC41-DFDB-43a5-8D26-0FE8B954DA51}'
|
|
ItemClsid = '{9CD4B2F4-923D-47f5-A062-E897DD1DAD50}'
|
|
SysvolDir = 'Registry'
|
|
FileName = 'Registry.xml'
|
|
CseGuid = '{B087BE9D-ED37-454F-AF9C-04291E351182}'
|
|
NameKey = 'Name'
|
|
}
|
|
LocalUsersAndGroups = @{
|
|
RootClsid = '{3125E937-EB16-4b4c-9934-544FC6D24D26}'
|
|
ItemClsid = '{6D4A79E4-529C-4481-ABD0-F5BD7EA93BA7}'
|
|
SysvolDir = 'Groups'
|
|
FileName = 'Groups.xml'
|
|
CseGuid = '{17D89FEC-5C44-4972-B12D-241CAEF74509}'
|
|
NameKey = 'GroupName'
|
|
}
|
|
}
|
|
|
|
function ConvertTo-ILTFilterXml {
|
|
<#
|
|
.SYNOPSIS
|
|
Converts a Filters array from a GPP item into the ILT <Filters> XML block.
|
|
Returns empty string if no filters are defined.
|
|
.DESCRIPTION
|
|
Supports SecurityGroup, OrgUnit, Computer, User, OperatingSystem, and WMI
|
|
filter types. SecurityGroup filters resolve names to SIDs at runtime.
|
|
#>
|
|
param(
|
|
[array]$Filters
|
|
)
|
|
|
|
if (-not $Filters -or $Filters.Count -eq 0) { return '' }
|
|
|
|
$esc = [System.Security.SecurityElement]
|
|
|
|
$filterXml = foreach ($f in $Filters) {
|
|
$bool = if ($f.Bool) { $f.Bool } else { 'AND' }
|
|
$not = if ($f.Not) { '1' } else { '0' }
|
|
$name = $esc::Escape($f.Name)
|
|
|
|
switch ($f.Type) {
|
|
'SecurityGroup' {
|
|
try {
|
|
$ntAccount = New-Object System.Security.Principal.NTAccount($f.Name)
|
|
$sid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
|
} catch {
|
|
throw "ILT Filter: cannot resolve group '$($f.Name)' to a SID: $_"
|
|
}
|
|
$userContext = if ($f.ContainsKey('UserContext') -and -not $f.UserContext) { '0' } else { '1' }
|
|
$primaryGroup = if ($f.PrimaryGroup) { '1' } else { '0' }
|
|
$localGroup = if ($f.LocalGroup) { '1' } else { '0' }
|
|
" <FilterGroup bool=`"$bool`" not=`"$not`" name=`"$name`" sid=`"$sid`" userContext=`"$userContext`" primaryGroup=`"$primaryGroup`" localGroup=`"$localGroup`"/>"
|
|
}
|
|
'OrgUnit' {
|
|
$type = if ($f.ContainsKey('OUType')) { $f.OUType } else { '' }
|
|
" <FilterOrgUnit bool=`"$bool`" not=`"$not`" name=`"$name`" type=`"$type`"/>"
|
|
}
|
|
'Computer' {
|
|
$type = if ($f.ContainsKey('NameType')) { $f.NameType } else { 'NETBIOS' }
|
|
" <FilterComputer bool=`"$bool`" not=`"$not`" type=`"$type`" name=`"$name`"/>"
|
|
}
|
|
'User' {
|
|
$type = if ($f.ContainsKey('NameType')) { $f.NameType } else { 'NETBIOS' }
|
|
" <FilterUser bool=`"$bool`" not=`"$not`" type=`"$type`" name=`"$name`"/>"
|
|
}
|
|
'OperatingSystem' {
|
|
$edition = if ($f.Edition) { $esc::Escape($f.Edition) } else { '' }
|
|
$version = if ($f.Version) { $esc::Escape($f.Version) } else { '' }
|
|
" <FilterOperatingSystem bool=`"$bool`" not=`"$not`" type=`"$name`" edition=`"$edition`" version=`"$version`"/>"
|
|
}
|
|
'WMI' {
|
|
$property = if ($f.Property) { $esc::Escape($f.Property) } else { '' }
|
|
$namespace = if ($f.Namespace) { $esc::Escape($f.Namespace) } else { 'root\CIMv2' }
|
|
$query = $esc::Escape($f.Query)
|
|
" <FilterWmi bool=`"$bool`" not=`"$not`" name=`"$name`" property=`"$property`" query=`"$query`" namespace=`"$namespace`"/>"
|
|
}
|
|
default {
|
|
Write-Host " [WARN] Unknown ILT filter type: $($f.Type)" -ForegroundColor Yellow
|
|
$null
|
|
}
|
|
}
|
|
}
|
|
|
|
$validFilters = @($filterXml | Where-Object { $_ })
|
|
if ($validFilters.Count -eq 0) { return '' }
|
|
|
|
return "`n <Filters>`n$($validFilters -join "`n")`n </Filters>"
|
|
}
|
|
|
|
function ConvertTo-ScheduledTaskXml {
|
|
<#
|
|
.SYNOPSIS
|
|
Generates ScheduledTasks.xml GPP content for TaskV2 entries.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[array]$Tasks,
|
|
|
|
[string]$Scope = 'Machine'
|
|
)
|
|
|
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
|
$esc = [System.Security.SecurityElement]
|
|
$userContext = if ($Scope -eq 'User') { '1' } else { '0' }
|
|
|
|
$itemsXml = foreach ($task in $Tasks) {
|
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
|
$action = $Script:GppActionCode[$task.Action]
|
|
$image = $Script:GppActionImage[$task.Action]
|
|
$name = $esc::Escape($task.Name)
|
|
$runAs = if ($task.RunAs) { $task.RunAs } else { 'NT AUTHORITY\System' }
|
|
$command = $esc::Escape($task.Command)
|
|
$arguments = if ($task.Arguments) { $esc::Escape($task.Arguments) } else { '' }
|
|
|
|
$trigger = switch ($task.Trigger) {
|
|
'AtStartup' { '<BootTrigger><Enabled>true</Enabled></BootTrigger>' }
|
|
'AtLogon' { '<LogonTrigger><Enabled>true</Enabled></LogonTrigger>' }
|
|
default { '<BootTrigger><Enabled>true</Enabled></BootTrigger>' }
|
|
}
|
|
|
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $task.Filters
|
|
|
|
@"
|
|
<TaskV2 clsid="{D8896631-B747-47a7-84A6-C155337F3BC8}" name="$name" image="$image" changed="$timestamp" uid="$uid" userContext="$userContext" removePolicy="0">
|
|
<Properties action="$action" name="$name" runAs="$runAs" logonType="S4U">
|
|
<Task version="1.2">
|
|
<RegistrationInfo><Description></Description></RegistrationInfo>
|
|
<Principals>
|
|
<Principal id="Author">
|
|
<UserId>$runAs</UserId>
|
|
<LogonType>S4U</LogonType>
|
|
<RunLevel>HighestAvailable</RunLevel>
|
|
</Principal>
|
|
</Principals>
|
|
<Settings>
|
|
<IdleSettings>
|
|
<Duration>PT10M</Duration>
|
|
<WaitTimeout>PT1H</WaitTimeout>
|
|
<StopOnIdleEnd>true</StopOnIdleEnd>
|
|
<RestartOnIdle>false</RestartOnIdle>
|
|
</IdleSettings>
|
|
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
<AllowHardTerminate>true</AllowHardTerminate>
|
|
<StartWhenAvailable>true</StartWhenAvailable>
|
|
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
<Enabled>true</Enabled>
|
|
<Hidden>false</Hidden>
|
|
<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
|
|
<Priority>7</Priority>
|
|
</Settings>
|
|
<Triggers>$trigger</Triggers>
|
|
<Actions>
|
|
<Exec>
|
|
<Command>$command</Command>
|
|
<Arguments>$arguments</Arguments>
|
|
</Exec>
|
|
</Actions>
|
|
</Task>
|
|
</Properties>$filterBlock
|
|
</TaskV2>
|
|
"@
|
|
}
|
|
|
|
return @"
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A71CD118DBCC}">
|
|
$($itemsXml -join "`n")
|
|
</ScheduledTasks>
|
|
"@
|
|
}
|
|
|
|
function ConvertTo-DriveMapXml {
|
|
<#
|
|
.SYNOPSIS
|
|
Generates Drives.xml GPP content for drive map entries.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[array]$Drives
|
|
)
|
|
|
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
|
$esc = [System.Security.SecurityElement]
|
|
|
|
$itemsXml = foreach ($drive in $Drives) {
|
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
|
$action = $Script:GppActionCode[$drive.Action]
|
|
$image = $Script:GppActionImage[$drive.Action]
|
|
$letter = $drive.Letter.TrimEnd(':')
|
|
$path = $esc::Escape($drive.Path)
|
|
$label = if ($drive.Label) { $esc::Escape($drive.Label) } else { '' }
|
|
$persistent = if ($drive.Reconnect) { '1' } else { '0' }
|
|
|
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $drive.Filters
|
|
|
|
@"
|
|
<Drive clsid="{935D1B74-9CB8-4e3c-9914-7DD559B7A417}" name="${letter}:" image="$image" changed="$timestamp" uid="$uid" userContext="1" removePolicy="0">
|
|
<Properties action="$action" thisDrive="NOCHANGE" allDrives="NOCHANGE" userName="" path="$path" label="$label" persistent="$persistent" useLetter="1" letter="$letter"/>$filterBlock
|
|
</Drive>
|
|
"@
|
|
}
|
|
|
|
return @"
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<Drives clsid="{8FDDCC1A-0C3C-43cd-A6B4-71A6DF20DA8C}">
|
|
$($itemsXml -join "`n")
|
|
</Drives>
|
|
"@
|
|
}
|
|
|
|
function ConvertTo-EnvironmentVariableXml {
|
|
<#
|
|
.SYNOPSIS
|
|
Generates EnvironmentVariables.xml GPP content.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[array]$Variables,
|
|
|
|
[string]$Scope = 'Machine'
|
|
)
|
|
|
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
|
$esc = [System.Security.SecurityElement]
|
|
$userContext = if ($Scope -eq 'User') { '1' } else { '0' }
|
|
$user = if ($Scope -eq 'User') { '1' } else { '0' }
|
|
|
|
$itemsXml = foreach ($var in $Variables) {
|
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
|
$action = $Script:GppActionCode[$var.Action]
|
|
$image = $Script:GppActionImage[$var.Action]
|
|
$name = $esc::Escape($var.Name)
|
|
$value = $esc::Escape($var.Value)
|
|
|
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $var.Filters
|
|
|
|
@"
|
|
<EnvironmentVariable clsid="{B1A3FA3F-E5D1-41ea-88D1-55CE96B6C78B}" name="$name" image="$image" changed="$timestamp" uid="$uid" userContext="$userContext">
|
|
<Properties action="$action" name="$name" value="$value" user="$user"/>$filterBlock
|
|
</EnvironmentVariable>
|
|
"@
|
|
}
|
|
|
|
return @"
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<EnvironmentVariables clsid="{D76B9641-3288-4f75-942D-087DE603E3EA}">
|
|
$($itemsXml -join "`n")
|
|
</EnvironmentVariables>
|
|
"@
|
|
}
|
|
|
|
function ConvertTo-ServiceXml {
|
|
<#
|
|
.SYNOPSIS
|
|
Generates Services.xml GPP content for NTService entries.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[array]$Services
|
|
)
|
|
|
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
|
$esc = [System.Security.SecurityElement]
|
|
$startupMap = @{
|
|
Automatic = 'AUTOMATIC'
|
|
Manual = 'MANUAL'
|
|
Disabled = 'DISABLED'
|
|
}
|
|
|
|
$itemsXml = foreach ($svc in $Services) {
|
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
|
$action = $Script:GppActionCode[$svc.Action]
|
|
$image = $Script:GppActionImage[$svc.Action]
|
|
$serviceName = $esc::Escape($svc.ServiceName)
|
|
$startupType = $startupMap[$svc.StartupType]
|
|
if (-not $startupType) { $startupType = 'NOCHANGE' }
|
|
|
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $svc.Filters
|
|
|
|
@"
|
|
<NTService clsid="{AB6F0B67-D4E1-4f59-A9E5-F6BD3A72F4FF}" name="$serviceName" image="$image" changed="$timestamp" uid="$uid" userContext="0" removePolicy="0">
|
|
<Properties startupType="$startupType" serviceName="$serviceName" serviceAction="NONE" timeout="30"/>$filterBlock
|
|
</NTService>
|
|
"@
|
|
}
|
|
|
|
return @"
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<NTServices clsid="{2CFB484A-4E86-4eb1-8B6A-E1535488BDBF}">
|
|
$($itemsXml -join "`n")
|
|
</NTServices>
|
|
"@
|
|
}
|
|
|
|
function ConvertTo-PrinterXml {
|
|
<#
|
|
.SYNOPSIS
|
|
Generates Printers.xml GPP content for shared printer entries.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[array]$Printers
|
|
)
|
|
|
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
|
$esc = [System.Security.SecurityElement]
|
|
|
|
$itemsXml = foreach ($printer in $Printers) {
|
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
|
$action = $Script:GppActionCode[$printer.Action]
|
|
$image = $Script:GppActionImage[$printer.Action]
|
|
$path = $esc::Escape($printer.Path)
|
|
# Derive name from last segment of UNC path
|
|
$name = ($printer.Path -split '\\' | Where-Object { $_ })[-1]
|
|
$default = if ($printer.Default) { '1' } else { '0' }
|
|
$skipLocal = if ($printer.SkipLocal) { '1' } else { '0' }
|
|
$comment = if ($printer.Comment) { $esc::Escape($printer.Comment) } else { '' }
|
|
$location = if ($printer.Location) { $esc::Escape($printer.Location) } else { '' }
|
|
|
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $printer.Filters
|
|
|
|
@"
|
|
<SharedPrinter clsid="{9A5E9697-9095-436d-A0EE-4D128FDFBCE5}" name="$name" image="$image" changed="$timestamp" uid="$uid" userContext="1" removePolicy="0">
|
|
<Properties action="$action" path="$path" comment="$comment" location="$location" default="$default" skipLocal="$skipLocal" deleteAll="0" persistent="0" deleteMaps="0" port=""/>$filterBlock
|
|
</SharedPrinter>
|
|
"@
|
|
}
|
|
|
|
return @"
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<Printers clsid="{1F577D12-3D1B-471e-A1B7-060317597B9C}">
|
|
$($itemsXml -join "`n")
|
|
</Printers>
|
|
"@
|
|
}
|
|
|
|
function ConvertTo-ShortcutXml {
|
|
<#
|
|
.SYNOPSIS
|
|
Generates Shortcuts.xml GPP content for shortcut entries.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[array]$Shortcuts,
|
|
|
|
[string]$Scope = 'User'
|
|
)
|
|
|
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
|
$esc = [System.Security.SecurityElement]
|
|
$userContext = if ($Scope -eq 'User') { '1' } else { '0' }
|
|
|
|
$itemsXml = foreach ($shortcut in $Shortcuts) {
|
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
|
$action = $Script:GppActionCode[$shortcut.Action]
|
|
$image = $Script:GppActionImage[$shortcut.Action]
|
|
$name = $esc::Escape($shortcut.Name)
|
|
$targetType = if ($shortcut.TargetType) { $shortcut.TargetType } else { 'FILESYSTEM' }
|
|
$targetPath = $esc::Escape($shortcut.TargetPath)
|
|
$shortcutPath = $esc::Escape($shortcut.ShortcutPath)
|
|
$arguments = if ($shortcut.Arguments) { $esc::Escape($shortcut.Arguments) } else { '' }
|
|
$startIn = if ($shortcut.StartIn) { $esc::Escape($shortcut.StartIn) } else { '' }
|
|
$comment = if ($shortcut.Comment) { $esc::Escape($shortcut.Comment) } else { '' }
|
|
$iconPath = if ($shortcut.IconPath) { $esc::Escape($shortcut.IconPath) } else { '' }
|
|
$iconIndex = if ($shortcut.IconIndex) { $shortcut.IconIndex } else { '0' }
|
|
|
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $shortcut.Filters
|
|
|
|
@"
|
|
<Shortcut clsid="{4F2F7C55-2790-433e-8127-0739D1CFA327}" name="$name" image="$image" changed="$timestamp" uid="$uid" userContext="$userContext" removePolicy="0">
|
|
<Properties action="$action" targetType="$targetType" targetPath="$targetPath" shortcutPath="$shortcutPath" arguments="$arguments" startIn="$startIn" comment="$comment" shortcutKey="0" iconPath="$iconPath" iconIndex="$iconIndex" window="" pidl=""/>$filterBlock
|
|
</Shortcut>
|
|
"@
|
|
}
|
|
|
|
return @"
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<Shortcuts clsid="{872ECB34-B2EC-401b-A585-D32574AA90EE}">
|
|
$($itemsXml -join "`n")
|
|
</Shortcuts>
|
|
"@
|
|
}
|
|
|
|
function ConvertTo-FileXml {
|
|
<#
|
|
.SYNOPSIS
|
|
Generates Files.xml GPP content for file copy/replace entries.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[array]$Files,
|
|
|
|
[string]$Scope = 'Machine'
|
|
)
|
|
|
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
|
$esc = [System.Security.SecurityElement]
|
|
$userContext = if ($Scope -eq 'User') { '1' } else { '0' }
|
|
|
|
$itemsXml = foreach ($file in $Files) {
|
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
|
$action = $Script:GppActionCode[$file.Action]
|
|
$image = $Script:GppActionImage[$file.Action]
|
|
$fromPath = $esc::Escape($file.FromPath)
|
|
$targetPath = $esc::Escape($file.TargetPath)
|
|
$readOnly = if ($file.ReadOnly) { '1' } else { '0' }
|
|
$hidden = if ($file.Hidden) { '1' } else { '0' }
|
|
|
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $file.Filters
|
|
|
|
@"
|
|
<File clsid="{50BE44C8-567A-4ed1-B1D0-9234FE1F38AF}" name="$targetPath" image="$image" changed="$timestamp" uid="$uid" userContext="$userContext" removePolicy="0">
|
|
<Properties action="$action" fromPath="$fromPath" targetPath="$targetPath" readOnly="$readOnly" archive="1" hidden="$hidden" suppress="1"/>$filterBlock
|
|
</File>
|
|
"@
|
|
}
|
|
|
|
return @"
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<Files clsid="{215B2E53-57CE-475c-80FE-9EEC14635851}">
|
|
$($itemsXml -join "`n")
|
|
</Files>
|
|
"@
|
|
}
|
|
|
|
function ConvertTo-NetworkShareXml {
|
|
<#
|
|
.SYNOPSIS
|
|
Generates NetworkShares.xml GPP content for network share entries.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[array]$Shares
|
|
)
|
|
|
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
|
$esc = [System.Security.SecurityElement]
|
|
|
|
$itemsXml = foreach ($share in $Shares) {
|
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
|
$action = $Script:GppActionCode[$share.Action]
|
|
$image = $Script:GppActionImage[$share.Action]
|
|
$name = $esc::Escape($share.Name)
|
|
$path = $esc::Escape($share.Path)
|
|
$comment = if ($share.Comment) { $esc::Escape($share.Comment) } else { '' }
|
|
$allRegular = if ($share.AllRegular) { $share.AllRegular } else { '' }
|
|
$allHidden = if ($share.AllHidden) { $share.AllHidden } else { '' }
|
|
$allAdminDrive = if ($share.AllAdminDrive) { $share.AllAdminDrive } else { '' }
|
|
$limitUsers = if ($share.LimitUsers) { $share.LimitUsers } else { '0' }
|
|
|
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $share.Filters
|
|
|
|
@"
|
|
<NetShare clsid="{2888C5E7-94FC-4739-90AA-2C1536D68BC0}" name="$name" image="$image" changed="$timestamp" uid="$uid" userContext="0" removePolicy="0">
|
|
<Properties action="$action" name="$name" path="$path" comment="$comment" allRegular="$allRegular" allHidden="$allHidden" allAdminDrive="$allAdminDrive" limitUsers="$limitUsers"/>$filterBlock
|
|
</NetShare>
|
|
"@
|
|
}
|
|
|
|
return @"
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<NetworkShareSettings clsid="{520870D8-A6E7-47e8-A8D8-E6A4E76EAEC2}">
|
|
$($itemsXml -join "`n")
|
|
</NetworkShareSettings>
|
|
"@
|
|
}
|
|
|
|
function ConvertTo-RegistryItemXml {
|
|
<#
|
|
.SYNOPSIS
|
|
Generates Registry.xml GPP content for registry preference items.
|
|
These are GPP Registry items (not Administrative Templates).
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[array]$Items,
|
|
|
|
[string]$Scope = 'Machine'
|
|
)
|
|
|
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
|
$esc = [System.Security.SecurityElement]
|
|
$userContext = if ($Scope -eq 'User') { '1' } else { '0' }
|
|
|
|
$itemsXml = foreach ($item in $Items) {
|
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
|
$action = $Script:GppActionCode[$item.Action]
|
|
$image = $Script:GppActionImage[$item.Action]
|
|
$hive = $esc::Escape($item.Hive)
|
|
$key = $esc::Escape($item.Key)
|
|
$name = if ($item.Name) { $esc::Escape($item.Name) } else { '' }
|
|
$type = if ($item.Type) { $item.Type } else { 'REG_SZ' }
|
|
$value = if ($item.Value) { $esc::Escape($item.Value) } else { '' }
|
|
$displayName = if ($name) { $name } else { '(Default)' }
|
|
|
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $item.Filters
|
|
|
|
@"
|
|
<Registry clsid="{9CD4B2F4-923D-47f5-A062-E897DD1DAD50}" name="$displayName" image="$image" changed="$timestamp" uid="$uid" userContext="$userContext" removePolicy="0">
|
|
<Properties action="$action" hive="$hive" key="$key" name="$name" type="$type" value="$value" displayDecimal="0" default="0"/>$filterBlock
|
|
</Registry>
|
|
"@
|
|
}
|
|
|
|
return @"
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<RegistrySettings clsid="{A3CCFC41-DFDB-43a5-8D26-0FE8B954DA51}">
|
|
$($itemsXml -join "`n")
|
|
</RegistrySettings>
|
|
"@
|
|
}
|
|
|
|
function ConvertTo-LocalGroupXml {
|
|
<#
|
|
.SYNOPSIS
|
|
Generates Groups.xml GPP content for local user/group management.
|
|
Supports ADD/REMOVE individual members (unlike RestrictedGroups
|
|
which enforces exact membership).
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[array]$Groups
|
|
)
|
|
|
|
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
|
$esc = [System.Security.SecurityElement]
|
|
|
|
$itemsXml = foreach ($group in $Groups) {
|
|
$uid = "{$([Guid]::NewGuid().ToString().ToUpper())}"
|
|
$action = $Script:GppActionCode[$group.Action]
|
|
$image = $Script:GppActionImage[$group.Action]
|
|
$groupName = $esc::Escape($group.GroupName)
|
|
$newName = if ($group.NewName) { $esc::Escape($group.NewName) } else { '' }
|
|
$description = if ($group.Description) { $esc::Escape($group.Description) } else { '' }
|
|
$deleteAllUsers = if ($group.DeleteAllUsers) { '1' } else { '0' }
|
|
$deleteAllGroups = if ($group.DeleteAllGroups) { '1' } else { '0' }
|
|
$removeAccounts = if ($group.RemoveAccounts) { '1' } else { '0' }
|
|
|
|
# Build <Members> block
|
|
$membersXml = ''
|
|
if ($group.Members -and $group.Members.Count -gt 0) {
|
|
$memberLines = foreach ($m in $group.Members) {
|
|
$memberName = $esc::Escape($m.Name)
|
|
$memberAction = $m.Action.ToUpper()
|
|
$sid = ''
|
|
try {
|
|
$ntAccount = New-Object System.Security.Principal.NTAccount($m.Name)
|
|
$sid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
|
} catch {
|
|
Write-Host " [WARN] Cannot resolve '$($m.Name)' to SID -- leaving empty" -ForegroundColor Yellow
|
|
}
|
|
" <Member name=`"$memberName`" action=`"$memberAction`" sid=`"$sid`"/>"
|
|
}
|
|
$membersXml = "`n <Members>`n$($memberLines -join "`n")`n </Members>"
|
|
}
|
|
|
|
$filterBlock = ConvertTo-ILTFilterXml -Filters $group.Filters
|
|
|
|
@"
|
|
<Group clsid="{6D4A79E4-529C-4481-ABD0-F5BD7EA93BA7}" name="$groupName" image="$image" changed="$timestamp" uid="$uid" userContext="0" removePolicy="0">
|
|
<Properties action="$action" groupName="$groupName" newName="$newName" description="$description" deleteAllUsers="$deleteAllUsers" deleteAllGroups="$deleteAllGroups" removeAccounts="$removeAccounts">$membersXml
|
|
</Properties>$filterBlock
|
|
</Group>
|
|
"@
|
|
}
|
|
|
|
return @"
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<Groups clsid="{3125E937-EB16-4b4c-9934-544FC6D24D26}">
|
|
$($itemsXml -join "`n")
|
|
</Groups>
|
|
"@
|
|
}
|
|
|
|
function Set-GPOPreferences {
|
|
<#
|
|
.SYNOPSIS
|
|
Writes Group Policy Preferences XML files to SYSVOL.
|
|
Supports 10 GPP types: ScheduledTasks, DriveMaps, EnvironmentVariables,
|
|
Services, Printers, Shortcuts, Files, NetworkShares, RegistryItems,
|
|
LocalUsersAndGroups.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$GPOName,
|
|
|
|
[Parameter(Mandatory)]
|
|
[hashtable]$Preferences,
|
|
|
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
|
)
|
|
|
|
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
|
|
$utf8Bom = [System.Text.UTF8Encoding]::new($true)
|
|
|
|
foreach ($typeName in $Preferences.Keys) {
|
|
$typeInfo = $Script:GppTypeInfo[$typeName]
|
|
if (-not $typeInfo) {
|
|
Write-Host " [WARN] Unknown preference type: $typeName" -ForegroundColor Yellow
|
|
continue
|
|
}
|
|
|
|
$items = $Preferences[$typeName]
|
|
if (-not $items -or $items.Count -eq 0) { continue }
|
|
|
|
# Group items by scope
|
|
$byScope = @{ Machine = @(); User = @() }
|
|
foreach ($item in $items) {
|
|
$scope = switch ($typeName) {
|
|
'DriveMaps' { 'User' }
|
|
'Printers' { 'User' }
|
|
'Services' { 'Machine' }
|
|
'NetworkShares' { 'Machine' }
|
|
'LocalUsersAndGroups' { 'Machine' }
|
|
default { if ($item.Scope -eq 'User') { 'User' } else { 'Machine' } }
|
|
}
|
|
$byScope[$scope] += $item
|
|
}
|
|
|
|
foreach ($scope in @('Machine', 'User')) {
|
|
$scopeItems = $byScope[$scope]
|
|
if ($scopeItems.Count -eq 0) { continue }
|
|
|
|
# Generate XML
|
|
$xml = switch ($typeName) {
|
|
'ScheduledTasks' { ConvertTo-ScheduledTaskXml -Tasks $scopeItems -Scope $scope }
|
|
'DriveMaps' { ConvertTo-DriveMapXml -Drives $scopeItems }
|
|
'EnvironmentVariables' { ConvertTo-EnvironmentVariableXml -Variables $scopeItems -Scope $scope }
|
|
'Services' { ConvertTo-ServiceXml -Services $scopeItems }
|
|
'Printers' { ConvertTo-PrinterXml -Printers $scopeItems }
|
|
'Shortcuts' { ConvertTo-ShortcutXml -Shortcuts $scopeItems -Scope $scope }
|
|
'Files' { ConvertTo-FileXml -Files $scopeItems -Scope $scope }
|
|
'NetworkShares' { ConvertTo-NetworkShareXml -Shares $scopeItems }
|
|
'RegistryItems' { ConvertTo-RegistryItemXml -Items $scopeItems -Scope $scope }
|
|
'LocalUsersAndGroups' { ConvertTo-LocalGroupXml -Groups $scopeItems }
|
|
}
|
|
|
|
# Write to SYSVOL
|
|
$prefDir = Join-Path $sysvolPath "$scope\Preferences\$($typeInfo.SysvolDir)"
|
|
if (-not (Test-Path $prefDir)) {
|
|
New-Item -ItemType Directory -Path $prefDir -Force | Out-Null
|
|
}
|
|
$xmlPath = Join-Path $prefDir $typeInfo.FileName
|
|
[System.IO.File]::WriteAllText($xmlPath, $xml, $utf8Bom)
|
|
Write-Host " Written: $scope\Preferences\$($typeInfo.SysvolDir)\$($typeInfo.FileName) ($($scopeItems.Count) item(s))" -ForegroundColor Green
|
|
|
|
# Register CSE extension GUIDs
|
|
Add-GPOExtensionGuids -GPOName $GPOName -CseGuid $typeInfo.CseGuid -ToolGuid $Script:GppToolGuid -Scope $scope -Domain $Domain
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function Compare-GPOPreferences {
|
|
<#
|
|
.SYNOPSIS
|
|
Compares desired Group Policy Preferences against what's currently
|
|
deployed in SYSVOL. Checks XML file existence and item presence by name.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$GPOName,
|
|
|
|
[Parameter(Mandatory)]
|
|
[hashtable]$Preferences,
|
|
|
|
[string]$Domain = (Get-ADDomain).DNSRoot
|
|
)
|
|
|
|
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
|
|
$diffs = @()
|
|
|
|
Write-Host " Comparing preferences..." -ForegroundColor Yellow
|
|
|
|
foreach ($typeName in $Preferences.Keys) {
|
|
$typeInfo = $Script:GppTypeInfo[$typeName]
|
|
if (-not $typeInfo) { continue }
|
|
|
|
$items = $Preferences[$typeName]
|
|
if (-not $items -or $items.Count -eq 0) { continue }
|
|
|
|
# Group items by scope
|
|
$byScope = @{ Machine = @(); User = @() }
|
|
foreach ($item in $items) {
|
|
$scope = switch ($typeName) {
|
|
'DriveMaps' { 'User' }
|
|
'Printers' { 'User' }
|
|
'Services' { 'Machine' }
|
|
'NetworkShares' { 'Machine' }
|
|
'LocalUsersAndGroups' { 'Machine' }
|
|
default { if ($item.Scope -eq 'User') { 'User' } else { 'Machine' } }
|
|
}
|
|
$byScope[$scope] += $item
|
|
}
|
|
|
|
foreach ($scope in @('Machine', 'User')) {
|
|
$scopeItems = $byScope[$scope]
|
|
if ($scopeItems.Count -eq 0) { continue }
|
|
|
|
$xmlPath = Join-Path $sysvolPath "$scope\Preferences\$($typeInfo.SysvolDir)\$($typeInfo.FileName)"
|
|
|
|
if (-not (Test-Path $xmlPath)) {
|
|
Write-Host " [DRIFT] Missing: $scope\Preferences\$($typeInfo.SysvolDir)\$($typeInfo.FileName)" -ForegroundColor Red
|
|
$diffs += [PSCustomObject]@{
|
|
Type = 'Preference'
|
|
PrefType = $typeName
|
|
Scope = $scope
|
|
Item = '(entire file)'
|
|
Status = 'Missing'
|
|
}
|
|
continue
|
|
}
|
|
|
|
# Parse existing XML and check for each desired item
|
|
try {
|
|
[xml]$existingXml = Get-Content $xmlPath -Raw
|
|
|
|
foreach ($item in $scopeItems) {
|
|
$itemName = $item.($typeInfo.NameKey)
|
|
|
|
# DriveMaps use "Z:" format in XML but settings use "Z"
|
|
$searchName = $itemName
|
|
if ($typeName -eq 'DriveMaps') {
|
|
$searchName = "$($itemName.TrimEnd(':')):"
|
|
}
|
|
|
|
$found = $existingXml.SelectNodes("//*[@name='$searchName']")
|
|
if (-not $found -or $found.Count -eq 0) {
|
|
Write-Host " [DRIFT] Missing $typeName item: $itemName" -ForegroundColor Red
|
|
$diffs += [PSCustomObject]@{
|
|
Type = 'Preference'
|
|
PrefType = $typeName
|
|
Scope = $scope
|
|
Item = $itemName
|
|
Status = 'Missing item'
|
|
}
|
|
} else {
|
|
Write-Host " [OK] $typeName`: $itemName" -ForegroundColor Green
|
|
}
|
|
}
|
|
} catch {
|
|
Write-Host " [DRIFT] Cannot parse: $($typeInfo.FileName) -- $($_.Exception.Message)" -ForegroundColor Red
|
|
$diffs += [PSCustomObject]@{
|
|
Type = 'Preference'
|
|
PrefType = $typeName
|
|
Scope = $scope
|
|
Item = '(parse error)'
|
|
Status = 'Invalid XML'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($diffs.Count -eq 0) {
|
|
Write-Host " [OK] All preferences match desired state" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " [DRIFT] $($diffs.Count) preference difference(s) found" -ForegroundColor Red
|
|
}
|
|
|
|
return $diffs
|
|
}
|