# 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 }