policies.ps1
|
#Requires -Version 5.0 function Resolve-LGPOSource { <# .SYNOPSIS Returns metadata for a known LGPO source location. .DESCRIPTION Pure data lookup. No I/O. Centralises the "where do we get LGPO from" decision so that every other function in this module can reference it without duplicating URLs or hashes. When Microsoft moves the file or publishes a new SCT release, update the table below and have a human review the diff. This function is the single point of change for supply-chain trust. .PARAMETER Source Source identifier. Currently only 'SCT-LGPO-Standalone' is recognised. .EXAMPLE PS> Resolve-LGPOSource .LINK https://github.com/adnoctem/winkit/lib/policies.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $false)] [ValidateSet('SCT-LGPO-Standalone')] [string] $Source = 'SCT-LGPO-Standalone' ) $sources = @{ 'SCT-LGPO-Standalone' = [PSCustomObject]@{ Name = 'Security Compliance Toolkit - LGPO standalone' Url = 'https://download.microsoft.com/download/8/5/C/85C25433-A1B0-4FFA-9429-7E023E7DA8D8/LGPO.zip' Sha256 = 'PLACEHOLDER_REPLACE_ON_FIRST_VENDORING' ExpectedBinaryPath = 'LGPO_30/LGPO.exe' LastVerified = '2026-06-14' } } if (-not $sources.ContainsKey($Source)) { throw "Unknown LGPO source '$Source'." } return $sources[$Source] } function Test-LGPOSourceAvailability { <# .SYNOPSIS Verifies the LGPO download URL is still reachable. .DESCRIPTION Issues a HEAD request to the URL returned by Resolve-LGPOSource. Does NOT download or verify the content - that's Install-LGPO's job. Intended for weekly CI runs. The output object is suitable for serialising to JSON and archiving as ISO supply-chain evidence ("we monitor external dependencies weekly"). .PARAMETER Source Source identifier forwarded to Resolve-LGPOSource. .EXAMPLE PS> Test-LGPOSourceAvailability .LINK https://github.com/adnoctem/winkit/lib/policies.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $false)] [ValidateSet('SCT-LGPO-Standalone')] [string] $Source = 'SCT-LGPO-Standalone' ) $info = Resolve-LGPOSource -Source $Source try { $response = Invoke-WebRequest -Uri $info.Url -Method Head -UseBasicParsing -ErrorAction Stop [PSCustomObject]@{ Source = $Source Url = $info.Url Available = $true StatusCode = [int]$response.StatusCode ContentLength = $response.Headers['Content-Length'] CheckedAt = (Get-Date).ToUniversalTime() } } catch { [PSCustomObject]@{ Source = $Source Url = $info.Url Available = $false Error = $_.Exception.Message CheckedAt = (Get-Date).ToUniversalTime() } } } function Install-LGPO { <# .SYNOPSIS Downloads, verifies, and installs LGPO.exe to a controllable path. .DESCRIPTION Hash-verified install. Refuses to proceed if the SHA-256 of the downloaded archive does not match Resolve-LGPOSource's recorded hash. Idempotent: re-running when LGPO.exe is already present at the destination returns the existing path unless -Force is specified. Writing to the default destination (%ProgramData%) requires administrator elevation. A non-elevated session can specify an alternate -Destination within the user's writeable scope. .PARAMETER Destination Directory where LGPO.exe ends up. Defaults to %ProgramData%\winkit\tools. Non-elevated callers should supply a user-writeable path. .PARAMETER Source Source identifier forwarded to Resolve-LGPOSource. .PARAMETER Force Re-download and re-install even if LGPO.exe is already present. .EXAMPLE PS> Install-LGPO .EXAMPLE PS> Install-LGPO -Force -Verbose .EXAMPLE PS> Install-LGPO -Destination "$env:LOCALAPPDATA\winkit\tools" .LINK https://github.com/adnoctem/winkit/lib/policies.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [CmdletBinding(SupportsShouldProcess)] [OutputType([string])] param ( [Parameter(Mandatory = $false)] [string] $Destination = (Join-Path -Path $env:ProgramData -ChildPath 'winkit\tools'), [Parameter(Mandatory = $false)] [ValidateSet('SCT-LGPO-Standalone')] [string] $Source = 'SCT-LGPO-Standalone', [Parameter(Mandatory = $false)] [switch] $Force ) $info = Resolve-LGPOSource -Source $Source $exePath = Join-Path -Path $Destination -ChildPath 'LGPO.exe' if (Test-Path -LiteralPath $exePath -PathType Leaf) { if (-not $Force) { Write-Verbose "LGPO.exe already present at '$exePath'; skipping download." return $exePath } Write-Verbose "LGPO.exe already present at '$exePath'; -Force supplied, re-downloading." } if (-not $PSCmdlet.ShouldProcess($Destination, "Install LGPO from $($info.Url)")) { return } if (-not (Read-ProcessElevation)) { Write-Error "Writing to '$Destination' requires administrator rights. Run the session elevated or supply a user-writeable -Destination such as '$(Join-Path -Path $env:LOCALAPPDATA -ChildPath 'winkit\tools')'." return } $null = New-Item -Path $Destination -ItemType Directory -Force -ErrorAction SilentlyContinue $zipPath = Join-Path -Path $env:TEMP -ChildPath "LGPO_$(New-Guid).zip" $extractDir = Join-Path -Path $env:TEMP -ChildPath "LGPO_extract_$(New-Guid)" try { Write-Verbose "Downloading LGPO from $($info.Url)..." Invoke-WebRequest -Uri $info.Url -OutFile $zipPath -UseBasicParsing -ErrorAction Stop $actualHash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash $expectedHash = $info.Sha256.ToUpperInvariant() if ($actualHash -ne $expectedHash) { throw "SHA-256 mismatch for $($info.Url). Expected '$expectedHash', got '$actualHash'. Refusing to install." } Write-Verbose 'SHA-256 hash matches. Extracting archive...' Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force $found = Get-ChildItem -Path $extractDir -Recurse -Filter 'LGPO.exe' | Select-Object -First 1 if (-not $found) { throw "LGPO.exe not found in archive at $($info.Url)." } Copy-Item -Path $found.FullName -Destination $exePath -Force Write-Verbose "Installed LGPO.exe to '$exePath'." } finally { if (Test-Path -LiteralPath $zipPath -PathType Leaf) { Remove-Item -LiteralPath $zipPath -Force -ErrorAction SilentlyContinue } if (Test-Path -LiteralPath $extractDir -PathType Container) { Remove-Item -LiteralPath $extractDir -Recurse -Force -ErrorAction SilentlyContinue } } return $exePath } function Test-LGPOInstalled { <# .SYNOPSIS Returns $true if LGPO.exe is present at the expected path. .PARAMETER Path Full path to LGPO.exe. Defaults to %ProgramData%\winkit\tools\LGPO.exe. .EXAMPLE PS> if (Test-LGPOInstalled) { Invoke-LGPO -PolicyPath .\policy.txt } .LINK https://github.com/adnoctem/winkit/lib/policies.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $false)] [string] $Path = (Join-Path -Path $env:ProgramData -ChildPath 'winkit\tools\LGPO.exe') ) return (Test-Path -LiteralPath $Path -PathType Leaf) } function Invoke-LGPO { <# .SYNOPSIS Applies a policy file or GPO backup directory using LGPO.exe. .DESCRIPTION Accepts a path to either a policy text file (applied via /t) or a directory containing a GPO backup (applied via /g). The argument is auto-detected based on whether PolicyPath is a file or directory. Returns a structured object describing the apply, suitable for logging as audit evidence. .PARAMETER PolicyPath Path to a policy text file or a directory containing a GPO backup. .PARAMETER LgpoExe Path to LGPO.exe. Defaults to %ProgramData%\winkit\tools\LGPO.exe. .EXAMPLE PS> Invoke-LGPO -PolicyPath .\resources\policies\01-telemetry.txt .LINK https://github.com/adnoctem/winkit/lib/policies.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [CmdletBinding(SupportsShouldProcess)] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)] [ValidateScript({ Test-Path -LiteralPath $_ })] [string] $PolicyPath, [Parameter(Mandatory = $false)] [string] $LgpoExe = (Join-Path -Path $env:ProgramData -ChildPath 'winkit\tools\LGPO.exe') ) if (-not (Test-Path -LiteralPath $LgpoExe -PathType Leaf)) { throw "LGPO.exe not found at '$LgpoExe'. Run Install-LGPO first." } $isDirectory = Test-Path -LiteralPath $PolicyPath -PathType Container $arg = if ($isDirectory) { '/g' } else { '/t' } if (-not $PSCmdlet.ShouldProcess($PolicyPath, "Apply via LGPO.exe ($arg)")) { return } $stdoutFile = Join-Path -Path $env:TEMP -ChildPath "lgpo_stdout_$(New-Guid).log" $stderrFile = Join-Path -Path $env:TEMP -ChildPath "lgpo_stderr_$(New-Guid).log" try { $proc = Start-Process -FilePath $LgpoExe ` -ArgumentList @($arg, $PolicyPath) ` -RedirectStandardOutput $stdoutFile ` -RedirectStandardError $stderrFile ` -Wait -NoNewWindow -PassThru [PSCustomObject]@{ PolicyPath = $PolicyPath Mode = if ($isDirectory) { 'GpoBackup' } else { 'TextSource' } ExitCode = $proc.ExitCode StdOut = (Get-Content -LiteralPath $stdoutFile -Raw -ErrorAction SilentlyContinue) StdErr = (Get-Content -LiteralPath $stderrFile -Raw -ErrorAction SilentlyContinue) AppliedAt = (Get-Date).ToUniversalTime() Success = ($proc.ExitCode -eq 0) } } finally { if (Test-Path -LiteralPath $stdoutFile -PathType Leaf) { Remove-Item -LiteralPath $stdoutFile -Force -ErrorAction SilentlyContinue } if (Test-Path -LiteralPath $stderrFile -PathType Leaf) { Remove-Item -LiteralPath $stderrFile -Force -ErrorAction SilentlyContinue } } } |