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

272 lines
9.8 KiB
PowerShell

# GPOScripts.ps1
# Startup/shutdown/logon/logoff script deployment to SYSVOL.
# Depends on: GPOCore.ps1 (Get-GPOSysvolPath, Add-GPOExtensionGuids)
function Set-GPOScripts {
<#
.SYNOPSIS
Deploys startup/shutdown/logon/logoff scripts to a GPO's SYSVOL path
and generates the corresponding psscripts.ini / scripts.ini files.
#>
param(
[Parameter(Mandatory)]
[string]$GPOName,
[Parameter(Mandatory)]
[hashtable]$Scripts,
[Parameter(Mandatory)]
[string]$SourceDir,
[string]$Domain = (Get-ADDomain).DNSRoot
)
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
# Map settings keys to SYSVOL paths and ini section names
$typeInfo = @{
MachineStartup = @{ Scope = 'Machine'; SubDir = 'Startup'; Section = 'Startup' }
MachineShutdown = @{ Scope = 'Machine'; SubDir = 'Shutdown'; Section = 'Shutdown' }
UserLogon = @{ Scope = 'User'; SubDir = 'Logon'; Section = 'Logon' }
UserLogoff = @{ Scope = 'User'; SubDir = 'Logoff'; Section = 'Logoff' }
}
# Script CSE GUID and tool extension GUIDs
$scriptCseGuid = '{42B5FAAE-6536-11D2-AE5A-0000F87571E3}'
$machineToolGuid = '{40B6664F-4972-11D1-A7CA-0000F87571E3}'
$userToolGuid = '{40B66650-4972-11D1-A7CA-0000F87571E3}'
# Group work by scope (Machine / User) for ini file generation
$scopeWork = @{
Machine = @{ PsSections = [ordered]@{}; CmdSections = [ordered]@{} }
User = @{ PsSections = [ordered]@{}; CmdSections = [ordered]@{} }
}
foreach ($type in $Scripts.Keys) {
$info = $typeInfo[$type]
if (-not $info) {
Write-Host " [WARN] Unknown script type: $type" -ForegroundColor Yellow
continue
}
$scope = $info.Scope
$section = $info.Section
$scriptDir = Join-Path $sysvolPath "$scope\Scripts\$($info.SubDir)"
if (-not (Test-Path $scriptDir)) {
New-Item -ItemType Directory -Path $scriptDir -Force | Out-Null
}
$psEntries = @()
$cmdEntries = @()
foreach ($script in $Scripts[$type]) {
$sourcePath = Join-Path $SourceDir $script.Source
$fileName = Split-Path $script.Source -Leaf
$destPath = Join-Path $scriptDir $fileName
$params = if ($script.Parameters) { $script.Parameters } else { '' }
Copy-Item -Path $sourcePath -Destination $destPath -Force
Write-Host " Copied: $fileName -> $scope\Scripts\$($info.SubDir)\" -ForegroundColor Green
$entry = @{ CmdLine = $fileName; Parameters = $params }
if ($fileName -match '\.ps1$') {
$psEntries += $entry
} else {
$cmdEntries += $entry
}
}
if ($psEntries.Count -gt 0) {
$scopeWork[$scope].PsSections[$section] = $psEntries
}
if ($cmdEntries.Count -gt 0) {
$scopeWork[$scope].CmdSections[$section] = $cmdEntries
}
}
# Generate ini files per scope
foreach ($scope in @('Machine', 'User')) {
$work = $scopeWork[$scope]
$scriptsDir = Join-Path $sysvolPath "$scope\Scripts"
# psscripts.ini -- PowerShell scripts
if ($work.PsSections.Count -gt 0) {
if (-not (Test-Path $scriptsDir)) {
New-Item -ItemType Directory -Path $scriptsDir -Force | Out-Null
}
$sb = [System.Text.StringBuilder]::new()
foreach ($section in $work.PsSections.Keys) {
[void]$sb.AppendLine("[$section]")
$idx = 0
foreach ($entry in $work.PsSections[$section]) {
[void]$sb.AppendLine("${idx}CmdLine=$($entry.CmdLine)")
[void]$sb.AppendLine("${idx}Parameters=$($entry.Parameters)")
$idx++
}
[void]$sb.AppendLine('')
}
$iniPath = Join-Path $scriptsDir 'psscripts.ini'
[System.IO.File]::WriteAllText($iniPath, $sb.ToString(), [System.Text.Encoding]::Unicode)
Write-Host " Written: $scope\Scripts\psscripts.ini" -ForegroundColor Green
}
# scripts.ini -- non-PowerShell scripts (.bat, .cmd, .exe)
if ($work.CmdSections.Count -gt 0) {
if (-not (Test-Path $scriptsDir)) {
New-Item -ItemType Directory -Path $scriptsDir -Force | Out-Null
}
$sb = [System.Text.StringBuilder]::new()
foreach ($section in $work.CmdSections.Keys) {
[void]$sb.AppendLine("[$section]")
$idx = 0
foreach ($entry in $work.CmdSections[$section]) {
[void]$sb.AppendLine("${idx}CmdLine=$($entry.CmdLine)")
[void]$sb.AppendLine("${idx}Parameters=$($entry.Parameters)")
$idx++
}
[void]$sb.AppendLine('')
}
$iniPath = Join-Path $scriptsDir 'scripts.ini'
[System.IO.File]::WriteAllText($iniPath, $sb.ToString(), [System.Text.Encoding]::Unicode)
Write-Host " Written: $scope\Scripts\scripts.ini" -ForegroundColor Green
}
# Update CSE extension GUIDs in AD
if ($work.PsSections.Count -gt 0 -or $work.CmdSections.Count -gt 0) {
$toolGuid = if ($scope -eq 'Machine') { $machineToolGuid } else { $userToolGuid }
Add-GPOExtensionGuids -GPOName $GPOName -CseGuid $scriptCseGuid -ToolGuid $toolGuid -Scope $scope -Domain $Domain
}
}
}
function Compare-GPOScripts {
<#
.SYNOPSIS
Compares desired scripts against what's currently deployed in a GPO's
SYSVOL path. Returns diff objects for missing or changed scripts.
#>
param(
[Parameter(Mandatory)]
[string]$GPOName,
[Parameter(Mandatory)]
[hashtable]$Scripts,
[Parameter(Mandatory)]
[string]$SourceDir,
[string]$Domain = (Get-ADDomain).DNSRoot
)
$sysvolPath = Get-GPOSysvolPath -GPOName $GPOName -Domain $Domain
$typeInfo = @{
MachineStartup = @{ Scope = 'Machine'; SubDir = 'Startup' }
MachineShutdown = @{ Scope = 'Machine'; SubDir = 'Shutdown' }
UserLogon = @{ Scope = 'User'; SubDir = 'Logon' }
UserLogoff = @{ Scope = 'User'; SubDir = 'Logoff' }
}
$diffs = @()
Write-Host " Comparing scripts..." -ForegroundColor Yellow
foreach ($type in $Scripts.Keys) {
$info = $typeInfo[$type]
if (-not $info) { continue }
$scriptDir = Join-Path $sysvolPath "$($info.Scope)\Scripts\$($info.SubDir)"
foreach ($script in $Scripts[$type]) {
$fileName = Split-Path $script.Source -Leaf
$sourcePath = Join-Path $SourceDir $script.Source
$destPath = Join-Path $scriptDir $fileName
if (-not (Test-Path $destPath)) {
Write-Host " [DRIFT] Missing: $fileName in $($info.Scope)\Scripts\$($info.SubDir)\" -ForegroundColor Red
$diffs += [PSCustomObject]@{
Type = 'Script'
ScriptType = $type
FileName = $fileName
Status = 'Missing'
}
continue
}
# Compare file content by hash
$sourceHash = (Get-FileHash -Path $sourcePath -Algorithm SHA256).Hash
$destHash = (Get-FileHash -Path $destPath -Algorithm SHA256).Hash
if ($sourceHash -ne $destHash) {
Write-Host " [DRIFT] Changed: $fileName in $($info.Scope)\Scripts\$($info.SubDir)\" -ForegroundColor Red
$diffs += [PSCustomObject]@{
Type = 'Script'
ScriptType = $type
FileName = $fileName
Status = 'Content changed'
}
} else {
Write-Host " [OK] $fileName" -ForegroundColor Green
}
}
}
# Check psscripts.ini / scripts.ini existence per scope
$scopeHasPs = @{ Machine = $false; User = $false }
$scopeHasCmd = @{ Machine = $false; User = $false }
foreach ($type in $Scripts.Keys) {
$info = $typeInfo[$type]
if (-not $info) { continue }
foreach ($script in $Scripts[$type]) {
$fileName = Split-Path $script.Source -Leaf
if ($fileName -match '\.ps1$') { $scopeHasPs[$info.Scope] = $true }
else { $scopeHasCmd[$info.Scope] = $true }
}
}
foreach ($scope in @('Machine', 'User')) {
$scriptsDir = Join-Path $sysvolPath "$scope\Scripts"
if ($scopeHasPs[$scope]) {
$iniPath = Join-Path $scriptsDir 'psscripts.ini'
if (-not (Test-Path $iniPath)) {
Write-Host " [DRIFT] Missing: $scope\Scripts\psscripts.ini" -ForegroundColor Red
$diffs += [PSCustomObject]@{
Type = 'ScriptIni'
Scope = $scope
FileName = 'psscripts.ini'
Status = 'Missing'
}
}
}
if ($scopeHasCmd[$scope]) {
$iniPath = Join-Path $scriptsDir 'scripts.ini'
if (-not (Test-Path $iniPath)) {
Write-Host " [DRIFT] Missing: $scope\Scripts\scripts.ini" -ForegroundColor Red
$diffs += [PSCustomObject]@{
Type = 'ScriptIni'
Scope = $scope
FileName = 'scripts.ini'
Status = 'Missing'
}
}
}
}
if ($diffs.Count -eq 0) {
Write-Host " [OK] All scripts match desired state" -ForegroundColor Green
} else {
Write-Host " [DRIFT] $($diffs.Count) script difference(s) found" -ForegroundColor Red
}
return $diffs
}