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