fix policy definitions (2)
Some checks failed
Release / build-release (push) Failing after 4s

This commit is contained in:
Ludwig Lehnert
2026-06-29 13:21:07 +00:00
parent 881647aa64
commit 073ed667cd
5 changed files with 300 additions and 58 deletions

51
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: Release
on:
push:
branches:
- main
- master
workflow_dispatch:
permissions:
contents: write
jobs:
build-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build dist
run: make dist
- name: Package release zip
run: |
short_sha="${GITHUB_SHA::7}"
archive="softwarekatalog-${short_sha}.zip"
echo "SHORT_SHA=${short_sha}" >> "${GITHUB_ENV}"
echo "ARCHIVE=${archive}" >> "${GITHUB_ENV}"
rm -rf package
mkdir -p package
cp LICENSE package/LICENSE
cp dist/setup.ps1 package/setup.ps1
cp -R PolicyDefinitions package/PolicyDefinitions
(cd package && zip -r "../${archive}" LICENSE setup.ps1 PolicyDefinitions)
- name: Create or update release
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh release view "${SHORT_SHA}" >/dev/null 2>&1; then
gh release upload "${SHORT_SHA}" "${ARCHIVE}" --clobber
else
gh release create "${SHORT_SHA}" "${ARCHIVE}" \
--target "${GITHUB_SHA}" \
--title "${SHORT_SHA}" \
--notes "Automated build for ${GITHUB_SHA}."
fi

9
Makefile Normal file
View File

@@ -0,0 +1,9 @@
.PHONY: dist clean
dist: dist/setup.ps1
dist/setup.ps1: setup.ps1 scripts/build-dist.sh
./scripts/build-dist.sh setup.ps1 dist/setup.ps1
clean:
rm -rf dist

View File

@@ -48,10 +48,18 @@ The client reads up to 500 catalog entries and 500 required entries.
## Install
Build the ASCII-only distribution wrapper:
```sh
make dist
```
Use `dist/setup.ps1` for deployment. It is ASCII-only and decodes the UTF-8 payload at runtime, avoiding Windows PowerShell 5.1 encoding issues with downloaded scripts.
Run as admin or as computer startup script:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\setup.ps1 -Mode Install
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\dist\setup.ps1 -Mode Install
```
Generated files are written to:

52
scripts/build-dist.sh Executable file
View File

@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
src="${1:-setup.ps1}"
out="${2:-dist/setup.ps1}"
out_dir="$(dirname "$out")"
mkdir -p "$out_dir"
if base64 --help 2>&1 | grep -q -- '-w'; then
payload="$(base64 -w 0 "$src")"
else
payload="$(base64 "$src" | tr -d '\n')"
fi
cat > "$out" <<EOF
# ASCII-only bootstrap generated from setup.ps1. Do not edit.
[CmdletBinding()]
param(
[ValidateSet('Install','Daemon','UpgradeAll','ApplyPolicy','ProcessRequest')]
[string]\$Mode = 'Install',
[ValidateSet('Install','Uninstall')]
[string]\$RequestAction = 'Install',
[string]\$AppAlias,
[string]\$RequestedBy,
[string]\$RequestedBySid
)
Set-StrictMode -Version Latest
\$ErrorActionPreference = 'Stop'
\$Payload = @'
$payload
'@
\$PayloadBytes = [Convert]::FromBase64String(\$Payload)
\$OutputBytes = New-Object byte[] (\$PayloadBytes.Length + 3)
\$OutputBytes[0] = 0xEF
\$OutputBytes[1] = 0xBB
\$OutputBytes[2] = 0xBF
[Array]::Copy(\$PayloadBytes, 0, \$OutputBytes, 3, \$PayloadBytes.Length)
\$TempScript = Join-Path ([System.IO.Path]::GetTempPath()) ('softkat-{0}.ps1' -f [Guid]::NewGuid().ToString('N'))
[System.IO.File]::WriteAllBytes(\$TempScript, \$OutputBytes)
try {
& \$TempScript -Mode \$Mode -RequestAction \$RequestAction -AppAlias \$AppAlias -RequestedBy \$RequestedBy -RequestedBySid \$RequestedBySid
}
finally {
Remove-Item -LiteralPath \$TempScript -Force -ErrorAction SilentlyContinue
}
EOF

236
setup.ps1
View File

@@ -3,7 +3,7 @@ SelfServiceWinget.ps1
Use as:
- Computer Startup GPO script (default mode = Install)
- Scheduled task target (Mode = Daemon / UpgradeAll / ApplyPolicy)
- Scheduled task target (Mode = Daemon / UpgradeAll / ApplyPolicy / ProcessRequest)
Behavior:
User -> GUI / request helper -> named pipe -> daemon task (SYSTEM) -> winget install --scope machine
@@ -20,8 +20,13 @@ C:\ProgramData\__Softwarekatalog\
[CmdletBinding()]
param(
[ValidateSet('Install','Daemon','UpgradeAll','ApplyPolicy')]
[string]$Mode = 'Install'
[ValidateSet('Install','Daemon','UpgradeAll','ApplyPolicy','ProcessRequest')]
[string]$Mode = 'Install',
[ValidateSet('Install','Uninstall')]
[string]$RequestAction = 'Install',
[string]$AppAlias,
[string]$RequestedBy,
[string]$RequestedBySid
)
Set-StrictMode -Version Latest
@@ -360,6 +365,77 @@ function Invoke-WithWingetLock {
}
}
function Test-WingetLockAvailable {
$mutex = New-Object System.Threading.Mutex($false, $WingetMutexName)
$hasLock = $false
try {
$hasLock = $mutex.WaitOne(0)
return $hasLock
}
finally {
if ($hasLock) {
$mutex.ReleaseMutex()
}
$mutex.Dispose()
}
}
function ConvertTo-ProcessArgument {
param([Parameter(Mandatory)][string]$Value)
if ($Value -match '[\s"]') {
return '"{0}"' -f ($Value.Replace('"', '\"'))
}
return $Value
}
function Get-WorkerScriptPath {
if (Test-Path -LiteralPath $LocalScriptPath) {
return $LocalScriptPath
}
if ($PSCommandPath -and (Test-Path -LiteralPath $PSCommandPath)) {
return $PSCommandPath
}
throw 'Could not determine worker script path.'
}
function Start-SoftwarekatalogWorker {
param(
[Parameter(Mandatory)][ValidateSet('ApplyPolicy','ProcessRequest')][string]$WorkerMode,
[ValidateSet('Install','Uninstall')][string]$WorkerAction = 'Install',
[string]$WorkerAlias,
[string]$WorkerRequestedBy,
[string]$WorkerRequestedBySid
)
$workerScript = Get-WorkerScriptPath
$args = @(
'-NoProfile',
'-ExecutionPolicy', 'Bypass',
'-File', $workerScript,
'-Mode', $WorkerMode
)
if ($WorkerMode -eq 'ProcessRequest') {
$args += @('-RequestAction', $WorkerAction, '-AppAlias', $WorkerAlias)
if (-not [string]::IsNullOrWhiteSpace($WorkerRequestedBy)) {
$args += @('-RequestedBy', $WorkerRequestedBy)
}
if (-not [string]::IsNullOrWhiteSpace($WorkerRequestedBySid)) {
$args += @('-RequestedBySid', $WorkerRequestedBySid)
}
}
$argumentLine = (@($args) | ForEach-Object { ConvertTo-ProcessArgument -Value ([string]$_) }) -join ' '
Start-Process -FilePath $PowerShellExe -ArgumentList $argumentLine -WindowStyle Hidden | Out-Null
}
function Get-WingetPath {
$candidates = @()
@@ -491,7 +567,7 @@ function Invoke-CatalogDaemon {
)
try {
`$client.Connect(30000)
`$client.Connect(60000)
`$reader = New-Object System.IO.StreamReader(`$client, `$encoding)
`$writer = New-Object System.IO.StreamWriter(`$client, `$encoding)
`$writer.AutoFlush = `$true
@@ -509,6 +585,9 @@ function Invoke-CatalogDaemon {
return `$response
}
catch [System.TimeoutException] {
throw 'Der Softwarekatalog-Dienst ist nicht erreichbar oder startet noch. Bitte versuchen Sie es erneut.'
}
finally {
`$client.Dispose()
}
@@ -535,10 +614,10 @@ if (-not `$AppAlias) {
`$response = Invoke-CatalogDaemon -Command `$Action -Alias `$alias
if (`$Action -eq 'Uninstall') {
Write-Host "Deinstallation für '`$alias' abgeschlossen."
Write-Host "Deinstallation für '`$alias' gestartet."
}
else {
Write-Host "Installation für '`$alias' abgeschlossen."
Write-Host "Installation für '`$alias' gestartet."
}
if (`$response.Message) {
@@ -687,7 +766,7 @@ function Invoke-CatalogDaemon {
)
try {
`$client.Connect(30000)
`$client.Connect(60000)
`$reader = New-Object System.IO.StreamReader(`$client, `$encoding)
`$writer = New-Object System.IO.StreamWriter(`$client, `$encoding)
`$writer.AutoFlush = `$true
@@ -705,6 +784,9 @@ function Invoke-CatalogDaemon {
return `$response
}
catch [System.TimeoutException] {
throw 'Der Softwarekatalog-Dienst ist nicht erreichbar oder startet noch. Bitte versuchen Sie es erneut.'
}
finally {
`$client.Dispose()
}
@@ -1047,10 +1129,10 @@ function Load-AppList {
`$status.Text = "Installation läuft: {0}" -f `$selected.DisplayName
`$response = New-AppRequest -Alias `$selected.Alias -Action 'Install'
`$status.Text = if (`$response.Message) { `$response.Message } else { "Installation abgeschlossen: {0}" -f `$selected.DisplayName }
`$status.Text = if (`$response.Message) { `$response.Message } else { "Installation gestartet: {0}" -f `$selected.DisplayName }
[System.Windows.Forms.MessageBox]::Show(
("Installation abgeschlossen:`r`n`r`n{0}`r`nAlias: {1}" -f `$selected.DisplayName, `$selected.Alias),
("Installation gestartet:`r`n`r`n{0}`r`nAlias: {1}" -f `$selected.DisplayName, `$selected.Alias),
`$CatalogTitle,
[System.Windows.Forms.MessageBoxButtons]::OK,
[System.Windows.Forms.MessageBoxIcon]::Information
@@ -1097,10 +1179,10 @@ function Load-AppList {
`$status.Text = "Deinstallation läuft: {0}" -f `$selected.DisplayName
`$response = New-AppRequest -Alias `$selected.Alias -Action 'Uninstall'
`$status.Text = if (`$response.Message) { `$response.Message } else { "Deinstallation abgeschlossen: {0}" -f `$selected.DisplayName }
`$status.Text = if (`$response.Message) { `$response.Message } else { "Deinstallation gestartet: {0}" -f `$selected.DisplayName }
[System.Windows.Forms.MessageBox]::Show(
("Deinstallation abgeschlossen:`r`n`r`n{0}`r`nAlias: {1}" -f `$selected.DisplayName, `$selected.Alias),
("Deinstallation gestartet:`r`n`r`n{0}`r`nAlias: {1}" -f `$selected.DisplayName, `$selected.Alias),
`$CatalogTitle,
[System.Windows.Forms.MessageBoxButtons]::OK,
[System.Windows.Forms.MessageBoxIcon]::Information
@@ -1740,21 +1822,18 @@ function Invoke-CatalogDaemonCommand {
'applypolicy' {
Write-Log "Authenticated policy apply requested by user='$($ClientIdentity.Name)' sid='$($ClientIdentity.Sid)'"
$wingetPath = Ensure-WingetAvailable
if (-not $wingetPath) {
throw 'winget is unavailable.'
if (-not (Test-WingetLockAvailable)) {
throw 'Der Softwarekatalog-Dienst ist gerade mit einer anderen winget-Aktion beschäftigt. Bitte später erneut versuchen.'
}
$result = Invoke-RequiredSoftwarePolicy -WingetPath $wingetPath
Start-SoftwarekatalogWorker -WorkerMode ApplyPolicy
return [pscustomobject]@{
Success = ($result.Failed -eq 0)
Success = $true
Command = $command
RequestedBy = $ClientIdentity.Name
RequestedBySid = $ClientIdentity.Sid
Installed = $result.Installed
Failed = $result.Failed
Skipped = $result.Skipped
Message = "Policy angewendet. Installiert=$($result.Installed), Fehler=$($result.Failed), Übersprungen=$($result.Skipped)."
Message = 'Policyanwendung gestartet.'
}
}
@@ -1781,34 +1860,11 @@ function Invoke-CatalogDaemonCommand {
$actionText = if ($command -eq 'uninstall') { 'uninstall' } else { 'install' }
Write-Log "Authenticated request: action=$actionText alias='$alias' package='$packageId' user='$($ClientIdentity.Name)' sid='$($ClientIdentity.Sid)'"
$wingetPath = Ensure-WingetAvailable
if (-not $wingetPath) {
throw 'winget is unavailable.'
if (-not (Test-WingetLockAvailable)) {
throw 'Der Softwarekatalog-Dienst ist gerade mit einer anderen winget-Aktion beschäftigt. Bitte später erneut versuchen.'
}
$result = Invoke-WithWingetLock -ScriptBlock {
if ($command -eq 'uninstall') {
$exitCode = Invoke-WingetUninstall -WingetPath $wingetPath -PackageId $packageId
}
else {
$exitCode = Invoke-WingetInstall -WingetPath $wingetPath -PackageId $packageId
}
$snapshotUpdated = $false
if ($exitCode -eq 0) {
$snapshotUpdated = Update-InstalledPackagesSnapshot -WingetPath $wingetPath
}
[pscustomobject]@{
ExitCode = $exitCode
SnapshotUpdated = [bool]$snapshotUpdated
}
}
$result = @($result)[-1]
if ($result.ExitCode -ne 0) {
throw "winget $actionText exited with code $($result.ExitCode) for package '$packageId'."
}
Start-SoftwarekatalogWorker -WorkerMode ProcessRequest -WorkerAction $command -WorkerAlias $alias -WorkerRequestedBy $ClientIdentity.Name -WorkerRequestedBySid $ClientIdentity.Sid
$verb = if ($command -eq 'uninstall') { 'Deinstallation' } else { 'Installation' }
return [pscustomobject]@{
@@ -1818,9 +1874,7 @@ function Invoke-CatalogDaemonCommand {
PackageId = $packageId
RequestedBy = $ClientIdentity.Name
RequestedBySid = $ClientIdentity.Sid
ExitCode = $result.ExitCode
SnapshotUpdated = $result.SnapshotUpdated
Message = "$verb abgeschlossen: $alias"
Message = "$verb gestartet: $alias"
}
}
@@ -1831,9 +1885,18 @@ function Invoke-CatalogDaemonCommand {
throw 'winget is unavailable.'
}
$result = Invoke-WithWingetLock -ScriptBlock {
$snapshotUpdated = Update-InstalledPackagesSnapshot -WingetPath $wingetPath
[pscustomobject]@{ SnapshotUpdated = [bool]$snapshotUpdated }
try {
$result = Invoke-WithWingetLock -TimeoutSeconds 2 -ScriptBlock {
$snapshotUpdated = Update-InstalledPackagesSnapshot -WingetPath $wingetPath
[pscustomobject]@{ SnapshotUpdated = [bool]$snapshotUpdated }
}
}
catch {
if ($_.Exception.Message -like 'Timed out waiting for winget lock*') {
throw 'Der Softwarekatalog-Dienst ist gerade mit einer anderen winget-Aktion beschäftigt. Bitte später erneut versuchen.'
}
throw
}
$result = @($result)[-1]
@@ -1860,13 +1923,10 @@ function Start-CatalogDaemon {
Write-Log "Named pipe daemon starting on '\\.\pipe\$PipeName'."
try {
$wingetPath = Ensure-WingetAvailable
if ($wingetPath) {
$null = Invoke-RequiredSoftwarePolicy -WingetPath $wingetPath
}
Start-SoftwarekatalogWorker -WorkerMode ApplyPolicy
}
catch {
Write-Log "Initial required software enforcement failed: $($_.Exception.Message)" 'WARN'
Write-Log "Could not start initial required software enforcement worker: $($_.Exception.Message)" 'WARN'
}
while ($true) {
@@ -1970,6 +2030,67 @@ function Invoke-ApplyPolicyMode {
}
}
function Invoke-ProcessRequestMode {
Ensure-Directory -Path $BaseDir
Ensure-Directory -Path $LogDir
if ([string]::IsNullOrWhiteSpace($AppAlias)) {
throw 'AppAlias is required for ProcessRequest mode.'
}
$alias = $AppAlias.Trim().ToLowerInvariant()
$app = Get-AppCatalogObjectByAlias -Alias $alias
if (-not $app) {
Write-Log "Worker rejected alias '$alias' because it is no longer in catalog policy. RequestedBy='$RequestedBy' Sid='$RequestedBySid'" 'WARN'
return
}
if ($RequestAction -eq 'Install' -and -not (Test-UserInstallsAllowed)) {
Write-Log "Worker rejected install for alias '$alias' because user installs are disabled. RequestedBy='$RequestedBy' Sid='$RequestedBySid'" 'WARN'
return
}
if ($RequestAction -eq 'Uninstall' -and -not (Test-UserUninstallAllowed)) {
Write-Log "Worker rejected uninstall for alias '$alias' because user uninstalls are disabled. RequestedBy='$RequestedBy' Sid='$RequestedBySid'" 'WARN'
return
}
$wingetPath = Ensure-WingetAvailable
if (-not $wingetPath) {
Write-Log "Worker aborting $RequestAction for alias '$alias' because winget is unavailable. RequestedBy='$RequestedBy' Sid='$RequestedBySid'" 'ERROR'
return
}
Write-Log "Worker starting action=$RequestAction alias='$alias' package='$($app.PackageId)' RequestedBy='$RequestedBy' Sid='$RequestedBySid'"
$result = Invoke-WithWingetLock -ScriptBlock {
if ($RequestAction -eq 'Uninstall') {
$exitCode = Invoke-WingetUninstall -WingetPath $wingetPath -PackageId ([string]$app.PackageId)
}
else {
$exitCode = Invoke-WingetInstall -WingetPath $wingetPath -PackageId ([string]$app.PackageId)
}
$snapshotUpdated = $false
if ($exitCode -eq 0) {
$snapshotUpdated = Update-InstalledPackagesSnapshot -WingetPath $wingetPath
}
[pscustomobject]@{
ExitCode = $exitCode
SnapshotUpdated = [bool]$snapshotUpdated
}
}
$result = @($result)[-1]
if ($result.ExitCode -eq 0) {
Write-Log "Worker completed action=$RequestAction alias='$alias' package='$($app.PackageId)' SnapshotUpdated=$($result.SnapshotUpdated) RequestedBy='$RequestedBy' Sid='$RequestedBySid'"
}
else {
Write-Log "Worker failed action=$RequestAction alias='$alias' package='$($app.PackageId)' code=$($result.ExitCode) RequestedBy='$RequestedBy' Sid='$RequestedBySid'" 'ERROR'
}
}
function Install-Service {
Ensure-Directory -Path $BaseDir
Ensure-Directory -Path $LogDir
@@ -2010,6 +2131,7 @@ try {
'Daemon' { Start-CatalogDaemon }
'UpgradeAll' { Invoke-UpgradeAllMode }
'ApplyPolicy' { Invoke-ApplyPolicyMode }
'ProcessRequest' { Invoke-ProcessRequestMode }
default { throw "Unsupported mode '$Mode'." }
}
}