- Katalog: Mozilla Firefox ESR (DE) - Office-Modus "Offline aus eigenem Pfad" (Netzlaufwerk/Ordner) mit Pruefung auf Office\Data und SourcePath in der XML-Vorschau - Seitenleiste: Speichern-Button + Hinweis bei ungespeicherten Aenderungen; Rueckfrage vor Laden einer anderen Blaupause, Beenden und Schliessen Co-Authored-By: Claude Opus 5.5 <[email protected]>
1482 lines
76 KiB
PowerShell
1482 lines
76 KiB
PowerShell
# KDS Install Agent - Kernfunktionen
|
|
# Wird vom Server (Agent.ps1) und von jedem Hintergrund-Job dot-sourced.
|
|
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
|
|
#region Allgemein ----------------------------------------------------------------
|
|
|
|
function Write-Log {
|
|
param([string]$Message, [ValidateSet('INFO', 'OK', 'WARN', 'ERROR')][string]$Level = 'INFO')
|
|
$line = '{0} [{1}] {2}' -f (Get-Date -Format 'HH:mm:ss'), $Level, $Message
|
|
if ($global:KdsLogFile) {
|
|
try { Add-Content -Path $global:KdsLogFile -Value $line -Encoding UTF8 } catch { }
|
|
}
|
|
Write-Host $line
|
|
}
|
|
|
|
function Get-KdsDataDir {
|
|
# Schreibbares Datenverzeichnis: bevorzugt auf dem Stick, sonst ProgramData
|
|
param([string]$Name)
|
|
$dir = Join-Path $global:KdsRoot $Name
|
|
try {
|
|
New-Item -ItemType Directory -Path $dir -Force -ErrorAction Stop | Out-Null
|
|
$probe = Join-Path $dir '.write-test'
|
|
Set-Content -Path $probe -Value 1 -ErrorAction Stop
|
|
Remove-Item $probe -Force
|
|
return $dir
|
|
} catch {
|
|
$dir = Join-Path $env:ProgramData "KDS-Agent\$Name"
|
|
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
|
return $dir
|
|
}
|
|
}
|
|
|
|
function Set-RegValue {
|
|
param([string]$Path, [string]$Name, $Value, [string]$Type = 'DWord')
|
|
if (-not (Test-Path $Path)) { New-Item -Path $Path -Force | Out-Null }
|
|
New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $Type -Force | Out-Null
|
|
}
|
|
|
|
function Test-True($v) { return ($v -eq $true -or $v -eq 'true' -or $v -eq 1) }
|
|
|
|
function Get-SystemInfo {
|
|
$os = Get-CimInstance Win32_OperatingSystem
|
|
$cs = Get-CimInstance Win32_ComputerSystem
|
|
$bios = Get-CimInstance Win32_BIOS
|
|
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
|
|
$disk = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='$env:SystemDrive'"
|
|
$tpm = $null
|
|
try { $t = Get-Tpm -ErrorAction Stop; $tpm = @{ present = $t.TpmPresent; ready = $t.TpmReady } } catch { }
|
|
$bat = Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
[pscustomobject]@{
|
|
computerName = $env:COMPUTERNAME
|
|
os = $os.Caption
|
|
build = "$($os.Version) ($((Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').DisplayVersion))"
|
|
manufacturer = $cs.Manufacturer
|
|
model = $cs.Model
|
|
serial = $bios.SerialNumber
|
|
cpu = $cpu.Name.Trim()
|
|
ramGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1)
|
|
diskFreeGB = [math]::Round($disk.FreeSpace / 1GB, 1)
|
|
diskSizeGB = [math]::Round($disk.Size / 1GB, 1)
|
|
domain = $cs.Domain
|
|
partOfDomain = $cs.PartOfDomain
|
|
tpm = $tpm
|
|
battery = if ($bat) { "$($bat.EstimatedChargeRemaining) %" } else { $null }
|
|
winget = [bool](Get-WingetPath)
|
|
user = "$env:USERDOMAIN\$env:USERNAME"
|
|
root = $global:KdsRoot
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region winget ------------------------------------------------------------------
|
|
|
|
function Get-WingetPath {
|
|
$c = Get-Command winget.exe -ErrorAction SilentlyContinue
|
|
if ($c) { return $c.Source }
|
|
$p = Get-ChildItem "$env:ProgramFiles\WindowsApps\Microsoft.DesktopAppInstaller_*_x64__8wekyb3d8bbwe\winget.exe" -ErrorAction SilentlyContinue |
|
|
Sort-Object FullName -Descending | Select-Object -First 1
|
|
if ($p) { return $p.FullName }
|
|
return $null
|
|
}
|
|
|
|
function Install-Winget {
|
|
if (Get-WingetPath) { return $true }
|
|
Write-Log 'winget nicht gefunden - starte Installation ...' 'WARN'
|
|
|
|
# 1) Offline-Pakete vom Stick (packages\winget\*.appx / *.msixbundle)
|
|
$offline = Join-Path $global:KdsRoot 'packages\winget'
|
|
if (Test-Path $offline) {
|
|
Get-ChildItem $offline -Filter *.appx | ForEach-Object {
|
|
try { Add-AppxPackage -Path $_.FullName -ErrorAction Stop; Write-Log " Abhaengigkeit installiert: $($_.Name)" } catch { }
|
|
}
|
|
Get-ChildItem $offline -Filter *.msixbundle | ForEach-Object {
|
|
try { Add-AppxPackage -Path $_.FullName -ErrorAction Stop; Write-Log " winget installiert (offline): $($_.Name)" 'OK' } catch { Write-Log " $_" 'WARN' }
|
|
}
|
|
if (Get-WingetPath) { return $true }
|
|
}
|
|
|
|
# 2) Offizieller Weg: Microsoft.WinGet.Client / Repair-WinGetPackageManager
|
|
try {
|
|
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope AllUsers | Out-Null
|
|
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
|
|
Install-Module Microsoft.WinGet.Client -Force -Scope AllUsers -AllowClobber
|
|
Import-Module Microsoft.WinGet.Client
|
|
Repair-WinGetPackageManager -AllUsers -Latest
|
|
Write-Log 'winget ueber Repair-WinGetPackageManager installiert' 'OK'
|
|
} catch {
|
|
Write-Log "Repair-WinGetPackageManager fehlgeschlagen: $_" 'WARN'
|
|
# 3) Fallback: direkter Download
|
|
try {
|
|
$tmp = Join-Path $env:TEMP 'kds-winget'; New-Item $tmp -ItemType Directory -Force | Out-Null
|
|
Invoke-WebRequest 'https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx' -OutFile "$tmp\vclibs.appx" -UseBasicParsing
|
|
try { Add-AppxPackage "$tmp\vclibs.appx" } catch { }
|
|
Invoke-WebRequest 'https://aka.ms/getwinget' -OutFile "$tmp\winget.msixbundle" -UseBasicParsing
|
|
Add-AppxPackage "$tmp\winget.msixbundle"
|
|
} catch { Write-Log "winget-Fallback fehlgeschlagen: $_" 'ERROR' }
|
|
}
|
|
return [bool](Get-WingetPath)
|
|
}
|
|
|
|
function Invoke-Winget {
|
|
# Fuehrt winget aus, schreibt bereinigte Ausgabe ins Log, gibt ExitCode zurueck
|
|
param([string[]]$Arguments)
|
|
$wg = Get-WingetPath
|
|
[Console]::OutputEncoding = [Text.Encoding]::UTF8
|
|
& $wg @Arguments 2>&1 | ForEach-Object {
|
|
$l = ("$_" -split "`r")[-1].TrimEnd()
|
|
if ($l -and $l -notmatch '^\s*[-\\|/]\s*$' -and $l -notmatch '[\u2588\u2592]') { Write-Log " $l" }
|
|
}
|
|
return $LASTEXITCODE
|
|
}
|
|
|
|
function Search-Winget {
|
|
param([string]$Query, [string]$Source = 'winget')
|
|
$wg = Get-WingetPath
|
|
if (-not $wg) { throw 'winget ist nicht installiert (Tab Updates -> winget installieren).' }
|
|
|
|
if (Get-Module -ListAvailable Microsoft.WinGet.Client) {
|
|
try {
|
|
Import-Module Microsoft.WinGet.Client -ErrorAction Stop
|
|
return @(Find-WinGetPackage -Query $Query -Source $Source -ErrorAction Stop | Select-Object -First 50 |
|
|
ForEach-Object { [pscustomobject]@{ name = $_.Name; id = $_.Id; version = $_.Version; source = $Source } })
|
|
} catch { }
|
|
}
|
|
|
|
[Console]::OutputEncoding = [Text.Encoding]::UTF8
|
|
$out = & $wg search --query $Query --source $Source --accept-source-agreements --disable-interactivity 2>$null
|
|
$lines = @($out | ForEach-Object { ("$_" -split "`r")[-1] })
|
|
$sep = -1
|
|
for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match '^-{5,}') { $sep = $i; break } }
|
|
if ($sep -lt 1) { return @() }
|
|
# Spaltenpositionen aus der (lokalisierten) Kopfzeile ermitteln
|
|
$starts = @([regex]::Matches($lines[$sep - 1], '\S+') | ForEach-Object { $_.Index })
|
|
if ($starts.Count -lt 3) { return @() }
|
|
$res = foreach ($l in $lines[($sep + 1)..($lines.Count - 1)]) {
|
|
if ($l.Length -lt $starts[2]) { continue }
|
|
$verEnd = if ($starts.Count -gt 3) { [math]::Min($starts[3], $l.Length) } else { $l.Length }
|
|
[pscustomobject]@{
|
|
name = $l.Substring(0, $starts[1]).Trim()
|
|
id = $l.Substring($starts[1], $starts[2] - $starts[1]).Trim()
|
|
version = $l.Substring($starts[2], $verEnd - $starts[2]).Trim()
|
|
source = $Source
|
|
}
|
|
}
|
|
return @($res | Where-Object { $_.id } | Select-Object -First 50)
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Programme ---------------------------------------------------------------
|
|
|
|
function Install-Apps {
|
|
param($Data)
|
|
$apps = @($Data.apps)
|
|
if (-not $apps.Count) { Write-Log 'Keine Programme ausgewaehlt.' 'WARN'; return }
|
|
$needWinget = @($apps | Where-Object { $_.source -ne 'local' }).Count -gt 0
|
|
if ($needWinget -and -not (Install-Winget)) { throw 'winget konnte nicht bereitgestellt werden.' }
|
|
if ($needWinget) { Invoke-Winget @('source', 'update') | Out-Null }
|
|
|
|
$ok = 0; $fail = @()
|
|
$n = 0
|
|
foreach ($a in $apps) {
|
|
$n++
|
|
Write-Log "[$n/$($apps.Count)] $($a.name)"
|
|
try {
|
|
if ($a.source -eq 'local') {
|
|
$file = $a.path
|
|
if (-not [IO.Path]::IsPathRooted($file)) { $file = Join-Path $global:KdsRoot $file }
|
|
if (-not (Test-Path $file)) { throw "Installer nicht gefunden: $file" }
|
|
$ext = [IO.Path]::GetExtension($file).ToLower()
|
|
$sp = @{ Wait = $true; PassThru = $true; WorkingDirectory = (Split-Path $file) }
|
|
switch ($ext) {
|
|
'.msi' { $sp.FilePath = 'msiexec.exe'; $sp.ArgumentList = "/i `"$file`" /qn /norestart $($a.args)" }
|
|
'.ps1' { $sp.FilePath = 'powershell.exe'; $sp.ArgumentList = "-NoProfile -ExecutionPolicy Bypass -File `"$file`" $($a.args)" }
|
|
{ $_ -in '.cmd', '.bat' } { $sp.FilePath = 'cmd.exe'; $sp.ArgumentList = "/c `"`"$file`" $($a.args)`"" }
|
|
{ $_ -in '.msix', '.msixbundle', '.appx', '.appxbundle' } { $sp = $null }
|
|
default { $sp.FilePath = $file; if ($a.args) { $sp.ArgumentList = $a.args } }
|
|
}
|
|
if ($sp) {
|
|
$code = (Start-Process @sp).ExitCode
|
|
} else {
|
|
# Paket fuer alle (auch kuenftige) Benutzer bereitstellen
|
|
Add-AppxProvisionedPackage -Online -PackagePath $file -SkipLicense | Out-Null
|
|
$code = 0
|
|
}
|
|
} else {
|
|
$src = if ($a.source) { $a.source } else { 'winget' }
|
|
$base = @('install', '--id', $a.id, '--exact', '--source', $src, '--silent',
|
|
'--accept-package-agreements', '--accept-source-agreements', '--disable-interactivity')
|
|
if ($a.override) { $base += @('--override', $a.override) }
|
|
$code = Invoke-Winget ($base + @('--scope', 'machine'))
|
|
# Kein systemweiter Installer verfuegbar -> ohne Scope erneut
|
|
if ($code -eq -1978335216 -or $code -eq -1978335212) {
|
|
Write-Log ' Kein Maschinen-Installer, versuche Standard-Scope ...'
|
|
$code = Invoke-Winget $base
|
|
}
|
|
}
|
|
# 0 = OK, 3010/1641 = Neustart noetig, -1978335189 = bereits aktuell/installiert
|
|
if ($code -in 0, 3010, 1641, -1978335189, -1978335135) { Write-Log " -> OK ($code)" 'OK'; $ok++ }
|
|
else { throw "ExitCode $code (0x{0:X8})" -f $code }
|
|
} catch {
|
|
Write-Log " -> Fehler: $_" 'ERROR'; $fail += $a.name
|
|
}
|
|
}
|
|
Write-Log "Programme: $ok erfolgreich, $($fail.Count) fehlgeschlagen $(if($fail){'(' + ($fail -join ', ') + ')'})" $(if ($fail) { 'WARN' } else { 'OK' })
|
|
}
|
|
|
|
function Update-AllApps {
|
|
param($Data)
|
|
if (-not (Install-Winget)) { throw 'winget nicht verfuegbar.' }
|
|
Write-Log 'Aktualisiere alle Programme ueber winget ...'
|
|
$code = Invoke-Winget @('upgrade', '--all', '--silent', '--include-unknown', '--accept-package-agreements', '--accept-source-agreements', '--disable-interactivity')
|
|
Write-Log "winget upgrade beendet (ExitCode $code)" 'OK'
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Office ------------------------------------------------------------------
|
|
|
|
function New-OfficeXml {
|
|
param($Cfg)
|
|
$e = { param($s) [Security.SecurityElement]::Escape([string]$s) }
|
|
$langs = @($Cfg.languages | Where-Object { $_ })
|
|
if (-not $langs.Count) { $langs = @('MatchOS') }
|
|
$langXml = ($langs | ForEach-Object { " <Language ID=`"$(& $e $_)`" />" }) -join "`r`n"
|
|
$exclXml = (@($Cfg.exclude) | Where-Object { $_ } | ForEach-Object { " <ExcludeApp ID=`"$(& $e $_)`" />" }) -join "`r`n"
|
|
|
|
$addAttr = "OfficeClientEdition=`"$($Cfg.arch)`" Channel=`"$(& $e $Cfg.channel)`""
|
|
if ($Cfg.sourcePath) { $addAttr += " SourcePath=`"$(& $e $Cfg.sourcePath)`"" }
|
|
if (Test-True $Cfg.migrateArch) { $addAttr += ' MigrateArch="TRUE"' }
|
|
|
|
$products = @()
|
|
$pidAttr = if ($Cfg.pidKey) { " PIDKEY=`"$(& $e $Cfg.pidKey)`"" } else { '' }
|
|
$products += " <Product ID=`"$(& $e $Cfg.product)`"$pidAttr>`r`n$langXml$(if($exclXml){"`r`n$exclXml"})`r`n </Product>"
|
|
foreach ($extra in @($Cfg.extraProducts)) {
|
|
if ($extra) { $products += " <Product ID=`"$(& $e $extra)`">`r`n$langXml`r`n </Product>" }
|
|
}
|
|
|
|
$x = @()
|
|
$x += "<Configuration ID=`"$([guid]::NewGuid())`">"
|
|
$x += " <Add $addAttr>"
|
|
$x += $products
|
|
$x += ' </Add>'
|
|
$x += " <Property Name=`"SharedComputerLicensing`" Value=`"$(if(Test-True $Cfg.sharedComputer){'1'}else{'0'})`" />"
|
|
$x += " <Property Name=`"FORCEAPPSHUTDOWN`" Value=`"TRUE`" />"
|
|
$x += " <Property Name=`"DeviceBasedLicensing`" Value=`"0`" />"
|
|
$x += " <Property Name=`"SCLCacheOverride`" Value=`"0`" />"
|
|
if ($Cfg.product -like '*Volume') { $x += " <Property Name=`"AUTOACTIVATE`" Value=`"1`" />" }
|
|
$x += " <Updates Enabled=`"$(if(Test-True $Cfg.updates){'TRUE'}else{'FALSE'})`" />"
|
|
if (Test-True $Cfg.removeMsi) { $x += ' <RemoveMSI />' }
|
|
$x += ' <AppSettings>'
|
|
if ($Cfg.company) { $x += " <Setup Name=`"Company`" Value=`"$(& $e $Cfg.company)`" />" }
|
|
if (Test-True $Cfg.ooxmlDefault) {
|
|
$x += ' <User Key="software\microsoft\office\16.0\common\general" Name="shownfirstrunoptin" Value="1" Type="REG_DWORD" App="office16" Id="L_DisableOptinWizard" />'
|
|
}
|
|
if (Test-True $Cfg.disableVba) {
|
|
foreach ($app in 'word', 'excel', 'powerpoint') {
|
|
$x += " <User Key=`"software\policies\microsoft\office\16.0\$app\security`" Name=`"blockcontentexecutionfrominternet`" Value=`"1`" Type=`"REG_DWORD`" App=`"$app`16`" Id=`"L_BlockMacroExecutionFromInternet`" />"
|
|
}
|
|
}
|
|
$x += ' </AppSettings>'
|
|
$x += " <Display Level=`"$(if(Test-True $Cfg.showUi){'Full'}else{'None'})`" AcceptEULA=`"TRUE`" />"
|
|
$x += '</Configuration>'
|
|
return ($x -join "`r`n")
|
|
}
|
|
|
|
function Get-OdtSetup {
|
|
$dir = Join-Path $global:KdsRoot 'office'
|
|
New-Item $dir -ItemType Directory -Force | Out-Null
|
|
$setup = Join-Path $dir 'setup.exe'
|
|
if (-not (Test-Path $setup)) {
|
|
Write-Log 'setup.exe (Office Deployment Tool) nicht vorhanden - lade von Microsoft CDN ...'
|
|
Invoke-WebRequest 'https://officecdn.microsoft.com/pr/wsus/setup.exe' -OutFile $setup -UseBasicParsing
|
|
Write-Log 'setup.exe heruntergeladen' 'OK'
|
|
}
|
|
return $setup
|
|
}
|
|
|
|
function Install-Office {
|
|
param($Data)
|
|
$cfg = $Data.office
|
|
$mode = if ($Data.mode) { $Data.mode } else { 'online' } # online | download | offline
|
|
$setup = Get-OdtSetup
|
|
$cfgDir = Join-Path $global:KdsRoot 'office\configs'
|
|
New-Item $cfgDir -ItemType Directory -Force | Out-Null
|
|
|
|
if ($mode -eq 'custom') {
|
|
# vorhandene Office-Quelle (z.B. Netzlaufwerk oder anderer Ordner) verwenden
|
|
$src = [Environment]::ExpandEnvironmentVariables([string]$Data.sourcePath).TrimEnd('\')
|
|
if (-not $src) { throw 'Kein Pfad zur Office-Quelle angegeben' }
|
|
if (-not (Test-Path (Join-Path $src 'Office\Data'))) { throw "Unter '$src' wurde kein Office-Build gefunden (erwartet: $src\Office\Data)" }
|
|
$cfg | Add-Member -NotePropertyName sourcePath -NotePropertyValue $src -Force
|
|
Write-Log "Office-Quelle: $src"
|
|
}
|
|
if ($mode -in 'download', 'offline') {
|
|
$src = Join-Path $global:KdsRoot "office\source\$($cfg.product)-$($cfg.arch)"
|
|
New-Item $src -ItemType Directory -Force | Out-Null
|
|
$cfg | Add-Member -NotePropertyName sourcePath -NotePropertyValue $src -Force
|
|
}
|
|
$xml = New-OfficeXml $cfg
|
|
$name = if ($Data.name) { $Data.name -replace '[^\w\-]', '_' } else { "$($cfg.product)-$($cfg.arch)" }
|
|
$xmlFile = Join-Path $cfgDir "$name.xml"
|
|
Set-Content -Path $xmlFile -Value $xml -Encoding UTF8
|
|
Write-Log "Konfiguration gespeichert: $xmlFile"
|
|
|
|
if ($mode -in 'download', 'offline') {
|
|
$hasData = Test-Path (Join-Path $cfg.sourcePath 'Office\Data')
|
|
if ($mode -eq 'download' -or -not $hasData) {
|
|
Write-Log "Lade Office-Installationsdateien nach $($cfg.sourcePath) (kann dauern) ..."
|
|
$p = Start-Process $setup -ArgumentList "/download `"$xmlFile`"" -Wait -PassThru -WindowStyle Hidden
|
|
if ($p.ExitCode -ne 0) { throw "setup.exe /download ExitCode $($p.ExitCode)" }
|
|
Write-Log 'Office-Build auf dem Stick abgeschlossen' 'OK'
|
|
}
|
|
if ($mode -eq 'download') { return }
|
|
}
|
|
|
|
Write-Log "Installiere $($cfg.product) ($($cfg.arch)-Bit, Kanal $($cfg.channel)) ..."
|
|
$p = Start-Process $setup -ArgumentList "/configure `"$xmlFile`"" -Wait -PassThru -WindowStyle Hidden
|
|
if ($p.ExitCode -ne 0) { throw "setup.exe /configure ExitCode $($p.ExitCode) - Logs unter $env:TEMP\$env:COMPUTERNAME-*.log" }
|
|
Write-Log 'Office installiert' 'OK'
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Desktop-Verknuepfungen ---------------------------------------------------
|
|
|
|
function Get-PublicDesktop { [Environment]::GetFolderPath('CommonDesktopDirectory') }
|
|
|
|
function Get-PublicShortcuts {
|
|
$d = Get-PublicDesktop
|
|
$sh = New-Object -ComObject WScript.Shell
|
|
@(Get-ChildItem $d -File | Where-Object { $_.Extension -in '.lnk', '.url', '.rdp' } | ForEach-Object {
|
|
$target = ''
|
|
if ($_.Extension -eq '.lnk') { $target = $sh.CreateShortcut($_.FullName).TargetPath }
|
|
else {
|
|
$pat = if ($_.Extension -eq '.rdp') { '^full address:s:(.*)$' } else { '^URL=(.*)$' }
|
|
$m = Select-String -Path $_.FullName -Pattern $pat | Select-Object -First 1
|
|
if ($m) { $target = [string]$m.Matches[0].Groups[1].Value }
|
|
}
|
|
[pscustomobject]@{ file = $_.Name; name = $_.BaseName; type = $_.Extension.TrimStart('.'); target = $target }
|
|
})
|
|
}
|
|
|
|
function Resolve-IconPath {
|
|
param([string]$Icon)
|
|
if (-not $Icon) { return $null }
|
|
$src = $Icon
|
|
if (-not [IO.Path]::IsPathRooted($src)) { $src = Join-Path $global:KdsRoot "icons\$Icon" }
|
|
if (-not (Test-Path $src)) { return $Icon } # z.B. "C:\Programme\x.exe,0"
|
|
# Icons vom Stick lokal ablegen, damit sie nach Abziehen bestehen bleiben
|
|
$dst = Join-Path $env:ProgramData 'KDS-Agent\Icons'
|
|
New-Item $dst -ItemType Directory -Force | Out-Null
|
|
Copy-Item $src $dst -Force
|
|
return (Join-Path $dst (Split-Path $src -Leaf))
|
|
}
|
|
|
|
function New-PublicShortcut {
|
|
param($S)
|
|
$d = Get-PublicDesktop
|
|
$safe = ($S.name -replace '[\\/:*?"<>|]', '_').Trim()
|
|
if (-not $safe) { throw 'Name fehlt' }
|
|
$icon = Resolve-IconPath $S.icon
|
|
if ($S.type -eq 'rdp') {
|
|
$file = Join-Path $d "$safe.rdp"
|
|
if ($S.rdpFile) {
|
|
# hochgeladene .rdp-Datei vom Stick verteilen
|
|
$src = Join-Path $global:KdsRoot "rdp\$(Split-Path $S.rdpFile -Leaf)"
|
|
if (-not (Test-Path $src)) { throw "RDP-Datei nicht gefunden: $src" }
|
|
Copy-Item $src $file -Force
|
|
} else {
|
|
Set-Content -Path $file -Value (New-RdpContent $S.rdp) -Encoding Unicode
|
|
}
|
|
} elseif ($S.type -eq 'url') {
|
|
$file = Join-Path $d "$safe.url"
|
|
$c = "[InternetShortcut]`r`nURL=$($S.target)`r`n"
|
|
if ($icon) { $c += "IconFile=$icon`r`nIconIndex=0`r`n" }
|
|
Set-Content -Path $file -Value $c -Encoding ASCII
|
|
} else {
|
|
$file = Join-Path $d "$safe.lnk"
|
|
$sh = New-Object -ComObject WScript.Shell
|
|
$lnk = $sh.CreateShortcut($file)
|
|
$lnk.TargetPath = [Environment]::ExpandEnvironmentVariables($S.target)
|
|
if ($S.args) { $lnk.Arguments = $S.args }
|
|
if ($S.workDir) { $lnk.WorkingDirectory = $S.workDir }
|
|
elseif (Test-Path $lnk.TargetPath -PathType Leaf) { $lnk.WorkingDirectory = Split-Path $lnk.TargetPath }
|
|
if ($icon) {
|
|
$icon = [Environment]::ExpandEnvironmentVariables($icon)
|
|
$lnk.IconLocation = if ($icon -match ',-?\d+$') { $icon } else { "$icon,0" }
|
|
}
|
|
$lnk.Save()
|
|
}
|
|
Write-Log "Verknuepfung erstellt: $file" 'OK'
|
|
return $file
|
|
}
|
|
|
|
function Remove-PublicShortcut {
|
|
param([string]$File)
|
|
$path = Join-Path (Get-PublicDesktop) (Split-Path $File -Leaf)
|
|
if (Test-Path $path) { Remove-Item $path -Force }
|
|
}
|
|
|
|
function Set-DesktopShortcuts {
|
|
param($Data)
|
|
foreach ($s in @($Data.shortcuts)) {
|
|
try { New-PublicShortcut $s | Out-Null } catch { Write-Log "Verknuepfung '$($s.name)': $_" 'ERROR' }
|
|
}
|
|
}
|
|
|
|
function New-RdpContent {
|
|
# Erzeugt den Inhalt einer .rdp-Datei aus den Formularwerten
|
|
param($R)
|
|
if (-not $R.host) { throw 'RDP: Server/Host fehlt' }
|
|
$b = { param($v) if (Test-True $v) { 1 } else { 0 } }
|
|
$addr = $R.host
|
|
if ($R.port -and "$($R.port)" -ne '3389') { $addr += ":$($R.port)" }
|
|
$l = @(
|
|
"full address:s:$addr"
|
|
"screen mode id:i:$(if (Test-True $R.fullscreen) { 2 } else { 1 })"
|
|
"use multimon:i:$(& $b $R.multimon)"
|
|
'dynamic resolution:i:1'
|
|
'smart sizing:i:1'
|
|
"redirectclipboard:i:$(& $b $R.clipboard)"
|
|
"redirectprinters:i:$(& $b $R.printers)"
|
|
"redirectsmartcards:i:$(& $b $R.smartcards)"
|
|
"drivestoredirect:s:$(if (Test-True $R.drives) { '*' } else { '' })"
|
|
"audiomode:i:$(switch ($R.audio) { 'remote' { 1 } 'none' { 2 } default { 0 } })"
|
|
"audiocapturemode:i:$(& $b $R.microphone)"
|
|
"camerastoredirect:s:$(if (Test-True $R.camera) { '*' } else { '' })"
|
|
'authentication level:i:2'
|
|
'prompt for credentials:i:0'
|
|
'promptcredentialonce:i:1'
|
|
'enablecredsspsupport:i:1'
|
|
'autoreconnection enabled:i:1'
|
|
)
|
|
if ($R.user) { $l += "username:s:$($R.user)" }
|
|
if ($R.gateway) {
|
|
$l += "gatewayhostname:s:$($R.gateway)", 'gatewayusagemethod:i:1', 'gatewaycredentialssource:i:4', 'gatewayprofileusagemethod:i:1', 'gatewaybrokeringtype:i:0'
|
|
} else { $l += 'gatewayusagemethod:i:4' }
|
|
if ($R.remoteApp) {
|
|
$l += 'remoteapplicationmode:i:1', "remoteapplicationprogram:s:||$($R.remoteApp)", "alternate shell:s:||$($R.remoteApp)",
|
|
"remoteapplicationname:s:$(if ($R.remoteAppName) { $R.remoteAppName } else { $R.remoteApp })", 'disableremoteappcapscheck:i:1'
|
|
}
|
|
if (Test-True $R.admin) { $l += 'administrative session:i:1' }
|
|
if ($R.extra) { $l += @($R.extra -split "`r?`n" | Where-Object { $_ -match '^[^:]+:[isb]:' }) }
|
|
return ($l -join "`r`n") + "`r`n"
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Dateien / Uploads / Blaupausen --------------------------------------------
|
|
|
|
$script:UploadTypes = @{
|
|
installers = '.exe', '.msi', '.msix', '.msixbundle', '.appx', '.appxbundle', '.ps1', '.cmd', '.bat'
|
|
icons = '.ico', '.exe', '.dll'
|
|
rdp = '.rdp'
|
|
vpn = '.ovpn', '.conf', '.crt', '.pem', '.key', '.p12', '.pfx'
|
|
}
|
|
|
|
function Resolve-UploadTarget {
|
|
param([string]$Kind, [string]$Name)
|
|
if (-not $script:UploadTypes.ContainsKey("$Kind")) { throw "Unbekannter Upload-Typ: $Kind" }
|
|
$n = ([IO.Path]::GetFileName("$Name") -replace '[^\w\-\.\(\)\+ ]', '_').Trim()
|
|
$ext = [IO.Path]::GetExtension($n).ToLower()
|
|
if (-not $n -or $ext -notin $script:UploadTypes[$Kind]) { throw "Dateityp '$ext' ist fuer $Kind nicht erlaubt ($($script:UploadTypes[$Kind] -join ', '))" }
|
|
return @((Join-Path $global:KdsRoot $Kind), $n)
|
|
}
|
|
|
|
function Get-InstallerMetaFile { Join-Path $global:KdsRoot 'installers\installers.json' }
|
|
|
|
function Get-InstallerMeta {
|
|
$f = Get-InstallerMetaFile
|
|
if (Test-Path $f) { return @([IO.File]::ReadAllText($f) | ConvertFrom-Json | ForEach-Object { $_ }) }
|
|
return @()
|
|
}
|
|
|
|
function Set-InstallerMeta {
|
|
# Name + Silent-Parameter je Installer merken (installers\installers.json)
|
|
param($M)
|
|
$list = @(Get-InstallerMeta | Where-Object { $_.file -ne $M.file })
|
|
if (-not $M.remove) { $list += [pscustomobject]@{ file = $M.file; name = $M.name; args = $M.args } }
|
|
New-Item (Split-Path (Get-InstallerMetaFile)) -ItemType Directory -Force | Out-Null
|
|
[IO.File]::WriteAllText((Get-InstallerMetaFile), (ConvertTo-Json -InputObject @($list) -Depth 3), (New-Object Text.UTF8Encoding($false)))
|
|
}
|
|
|
|
function Get-LocalInstallers {
|
|
$dir = Join-Path $global:KdsRoot 'installers'
|
|
$meta = Get-InstallerMeta
|
|
@(Get-ChildItem $dir -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.Extension.ToLower() -in $script:UploadTypes.installers } | ForEach-Object {
|
|
$rel = $_.FullName.Substring($global:KdsRoot.Length + 1)
|
|
$m = $meta | Where-Object file -eq $rel | Select-Object -First 1
|
|
[pscustomobject]@{
|
|
file = $rel
|
|
name = if ($m.name) { [string]$m.name } else { $_.BaseName }
|
|
args = if ($m) { [string]$m.args } else { '' }
|
|
sizeMB = [math]::Round($_.Length / 1MB, 1)
|
|
}
|
|
})
|
|
}
|
|
|
|
function Get-BlueprintPath {
|
|
param([string]$Id)
|
|
$safe = ($Id -replace '[^\w\-\. ]', '_').Trim(' .')
|
|
if (-not $safe) { throw 'Ungueltiger Name' }
|
|
return (Join-Path $global:KdsRoot "blueprints\$safe.json")
|
|
}
|
|
|
|
function Get-Blueprints {
|
|
@(Get-ChildItem (Join-Path $global:KdsRoot 'blueprints') -Filter *.json -ErrorAction SilentlyContinue | ForEach-Object {
|
|
$f = $_
|
|
try {
|
|
$j = [IO.File]::ReadAllText($f.FullName) | ConvertFrom-Json
|
|
[pscustomobject]@{
|
|
id = $f.BaseName
|
|
name = if ($j.name) { [string]$j.name } else { $f.BaseName }
|
|
customer = [string]$j.customer
|
|
description = [string]$j.description
|
|
apps = @($j.apps).Where({ $_ }).Count
|
|
shortcuts = @($j.shortcuts).Where({ $_ }).Count
|
|
office = [string]$j.office.product
|
|
steps = @($j.steps)
|
|
updated = $f.LastWriteTime.ToString('dd.MM.yyyy HH:mm')
|
|
}
|
|
} catch { Write-Log "Blaupause $($f.Name) fehlerhaft: $_" 'WARN' }
|
|
})
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region System / Sicherheit -----------------------------------------------------
|
|
|
|
function Set-SystemConfig {
|
|
param($Data)
|
|
$c = $Data.system
|
|
if ($c.timeZone) { Set-TimeZone -Id $c.timeZone; Write-Log "Zeitzone: $($c.timeZone)" 'OK' }
|
|
if (Test-True $c.showExtensions) {
|
|
# Default-Profil + aktueller Benutzer
|
|
Set-RegValue 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' 'HideFileExt' 0
|
|
reg load HKU\KdsDefault "$env:SystemDrive\Users\Default\NTUSER.DAT" | Out-Null
|
|
reg add 'HKU\KdsDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' /v HideFileExt /t REG_DWORD /d 0 /f | Out-Null
|
|
[gc]::Collect(); reg unload HKU\KdsDefault | Out-Null
|
|
Write-Log 'Dateiendungen werden angezeigt (inkl. neue Benutzer)' 'OK'
|
|
}
|
|
if (Test-True $c.disableConsumer) {
|
|
Set-RegValue 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent' 'DisableWindowsConsumerFeatures' 1
|
|
Write-Log 'Consumer-Features / Werbe-Apps deaktiviert' 'OK'
|
|
}
|
|
if ($c.computerName -and $c.computerName -ne $env:COMPUTERNAME) {
|
|
Rename-Computer -NewName $c.computerName -Force
|
|
Write-Log "Computer wird nach Neustart umbenannt in '$($c.computerName)'" 'OK'
|
|
}
|
|
}
|
|
|
|
function Set-SecurityConfig {
|
|
param($Data)
|
|
$c = $Data.security
|
|
if (Test-True $c.defender) {
|
|
try {
|
|
Set-MpPreference -DisableRealtimeMonitoring $false -PUAProtection Enabled -MAPSReporting Advanced -SubmitSamplesConsent SendSafeSamples -ErrorAction Stop
|
|
Write-Log 'Defender: Echtzeitschutz, PUA-Schutz, Cloud-Schutz aktiv' 'OK'
|
|
Update-MpSignature -ErrorAction SilentlyContinue
|
|
} catch { Write-Log "Defender: $_ (evtl. Drittanbieter-AV aktiv)" 'WARN' }
|
|
}
|
|
if (Test-True $c.firewall) {
|
|
Set-NetFirewallProfile -Profile Domain, Private, Public -Enabled True
|
|
Write-Log 'Firewall fuer alle Profile aktiviert' 'OK'
|
|
}
|
|
if (Test-True $c.smartScreen) {
|
|
Set-RegValue 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System' 'EnableSmartScreen' 1
|
|
Set-RegValue 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System' 'ShellSmartScreenLevel' 'Warn' 'String'
|
|
Write-Log 'SmartScreen aktiviert' 'OK'
|
|
}
|
|
if (Test-True $c.disableSmb1) {
|
|
try { Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -NoRestart -ErrorAction Stop | Out-Null } catch { }
|
|
Set-SmbServerConfiguration -EnableSMB1Protocol $false -Force -ErrorAction SilentlyContinue
|
|
Write-Log 'SMBv1 deaktiviert' 'OK'
|
|
}
|
|
if (Test-True $c.uac) {
|
|
$p = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
|
|
Set-RegValue $p 'EnableLUA' 1; Set-RegValue $p 'ConsentPromptBehaviorAdmin' 2; Set-RegValue $p 'PromptOnSecureDesktop' 1
|
|
Write-Log 'UAC auf hoechste Stufe gesetzt' 'OK'
|
|
}
|
|
if (Test-True $c.disableAutorun) {
|
|
$p = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer'
|
|
Set-RegValue $p 'NoDriveTypeAutoRun' 255; Set-RegValue $p 'NoAutorun' 1
|
|
Write-Log 'Autorun/Autoplay deaktiviert' 'OK'
|
|
}
|
|
if (Test-True $c.lsaProtection) {
|
|
Set-RegValue 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' 'RunAsPPL' 1
|
|
Write-Log 'LSA-Schutz aktiviert (Neustart noetig)' 'OK'
|
|
}
|
|
if (Test-True $c.telemetryMin) {
|
|
Set-RegValue 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection' 'AllowTelemetry' 1
|
|
Write-Log 'Telemetrie auf "Erforderlich" reduziert' 'OK'
|
|
}
|
|
switch ($c.rdp) {
|
|
'enable' {
|
|
Set-RegValue 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' 'fDenyTSConnections' 0
|
|
Set-RegValue 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' 'UserAuthentication' 1
|
|
Enable-NetFirewallRule -DisplayGroup '@FirewallAPI.dll,-28752' -ErrorAction SilentlyContinue
|
|
Write-Log 'Remotedesktop aktiviert (mit NLA)' 'OK'
|
|
}
|
|
'disable' {
|
|
Set-RegValue 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' 'fDenyTSConnections' 1
|
|
Disable-NetFirewallRule -DisplayGroup '@FirewallAPI.dll,-28752' -ErrorAction SilentlyContinue
|
|
Write-Log 'Remotedesktop deaktiviert' 'OK'
|
|
}
|
|
}
|
|
if ($c.localAdminName -and $c.localAdminPassword) {
|
|
$pw = ConvertTo-SecureString $c.localAdminPassword -AsPlainText -Force
|
|
if (Get-LocalUser -Name $c.localAdminName -ErrorAction SilentlyContinue) {
|
|
Set-LocalUser -Name $c.localAdminName -Password $pw
|
|
} else {
|
|
New-LocalUser -Name $c.localAdminName -Password $pw -PasswordNeverExpires -AccountNeverExpires | Out-Null
|
|
}
|
|
$grp = (Get-LocalGroup -SID 'S-1-5-32-544').Name
|
|
Add-LocalGroupMember -Group $grp -Member $c.localAdminName -ErrorAction SilentlyContinue
|
|
Write-Log "Lokaler Admin '$($c.localAdminName)' eingerichtet" 'OK'
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Energie -----------------------------------------------------------------
|
|
|
|
function Set-PowerConfig {
|
|
param($Data)
|
|
$c = $Data.power
|
|
$plans = @{ balanced = '381b4222-f694-41f0-9685-ff5bb260df2e'; high = '8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c'; ultimate = 'e9a42b02-d5df-448d-aa00-03f14749eb61' }
|
|
if ($c.plan -and $plans[$c.plan]) {
|
|
$guid = $plans[$c.plan]
|
|
powercfg /setactive $guid 2>$null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
# Plan ausgeblendet (z.B. Modern Standby) -> duplizieren
|
|
$o = powercfg /duplicatescheme $guid 2>$null
|
|
if ($o -match '([0-9a-f\-]{36})') { powercfg /setactive $Matches[1] }
|
|
}
|
|
Write-Log "Energieplan: $($c.plan)" $(if ($LASTEXITCODE -eq 0) { 'OK' } else { 'WARN' })
|
|
}
|
|
$map = @{ monitorAc = 'monitor-timeout-ac'; monitorDc = 'monitor-timeout-dc'; sleepAc = 'standby-timeout-ac'; sleepDc = 'standby-timeout-dc'; hibernateAc = 'hibernate-timeout-ac'; hibernateDc = 'hibernate-timeout-dc' }
|
|
foreach ($k in $map.Keys) {
|
|
$v = $c.$k
|
|
if ($null -ne $v -and "$v" -ne '') { powercfg /change $map[$k] ([int]$v); Write-Log " $($map[$k]) = $v min" }
|
|
}
|
|
$actions = @{ nothing = 0; sleep = 1; hibernate = 2; shutdown = 3 }
|
|
foreach ($pair in @(@('lidAc', '/setacvalueindex', 'LIDACTION'), @('lidDc', '/setdcvalueindex', 'LIDACTION'),
|
|
@('buttonAc', '/setacvalueindex', 'PBUTTONACTION'), @('buttonDc', '/setdcvalueindex', 'PBUTTONACTION'))) {
|
|
$v = $c.($pair[0])
|
|
if ($v -and $actions.ContainsKey($v)) {
|
|
powercfg $pair[1] SCHEME_CURRENT SUB_BUTTONS $pair[2] $actions[$v]
|
|
Write-Log " $($pair[2]) $($pair[0]) = $v"
|
|
}
|
|
}
|
|
powercfg /setactive SCHEME_CURRENT
|
|
if ($c.hibernate -in 'on', 'off') { powercfg /hibernate $c.hibernate; Write-Log "Ruhezustand: $($c.hibernate)" 'OK' }
|
|
if ($c.fastStartup -in 'on', 'off') {
|
|
Set-RegValue 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power' 'HiberbootEnabled' $(if ($c.fastStartup -eq 'on') { 1 } else { 0 })
|
|
Write-Log "Schnellstart: $($c.fastStartup)" 'OK'
|
|
}
|
|
Write-Log 'Energieeinstellungen angewendet' 'OK'
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region BitLocker ---------------------------------------------------------------
|
|
|
|
function Get-BitLockerInfo {
|
|
$info = [ordered]@{ available = $false; edition = (Get-CimInstance Win32_OperatingSystem).Caption }
|
|
try { $t = Get-Tpm -ErrorAction Stop; $info.tpmPresent = $t.TpmPresent; $info.tpmReady = $t.TpmReady } catch { $info.tpmPresent = $false }
|
|
if (-not (Get-Command Get-BitLockerVolume -ErrorAction SilentlyContinue)) {
|
|
$info.message = 'BitLocker-Cmdlets nicht verfuegbar (Windows Home?)'
|
|
return [pscustomobject]$info
|
|
}
|
|
$info.available = $true
|
|
$info.volumes = @(Get-BitLockerVolume | ForEach-Object {
|
|
[pscustomobject]@{
|
|
mount = $_.MountPoint
|
|
type = "$($_.VolumeType)"
|
|
status = "$($_.VolumeStatus)"
|
|
protection = "$($_.ProtectionStatus)"
|
|
percent = $_.EncryptionPercentage
|
|
method = "$($_.EncryptionMethod)"
|
|
protectors = @($_.KeyProtector | ForEach-Object { "$($_.KeyProtectorType)" })
|
|
}
|
|
})
|
|
return [pscustomobject]$info
|
|
}
|
|
|
|
function Save-RecoveryKey {
|
|
param([string]$Mount)
|
|
$vol = Get-BitLockerVolume -MountPoint $Mount
|
|
$rp = @($vol.KeyProtector | Where-Object KeyProtectorType -eq 'RecoveryPassword')
|
|
$dir = Get-KdsDataDir 'bitlocker-keys'
|
|
$file = Join-Path $dir ("{0}_{1}_{2}.txt" -f $env:COMPUTERNAME, ($Mount.TrimEnd(':\')), (Get-Date -Format 'yyyyMMdd-HHmm'))
|
|
$sn = (Get-CimInstance Win32_BIOS).SerialNumber
|
|
$txt = @("BitLocker-Wiederherstellungsschluessel", "Computer: $env:COMPUTERNAME", "Seriennummer: $sn", "Laufwerk: $Mount", "Datum: $(Get-Date)", '')
|
|
foreach ($k in $rp) { $txt += "ID: $($k.KeyProtectorId)"; $txt += "Schluessel: $($k.RecoveryPassword)"; $txt += '' }
|
|
Set-Content -Path $file -Value $txt -Encoding UTF8
|
|
Write-Log "Wiederherstellungsschluessel gespeichert: $file" 'OK'
|
|
return $rp
|
|
}
|
|
|
|
function Set-BitLockerConfig {
|
|
param($Data)
|
|
$c = $Data.bitlocker
|
|
$mount = if ($c.mount) { $c.mount } else { $env:SystemDrive }
|
|
if (-not (Get-Command Enable-BitLocker -ErrorAction SilentlyContinue)) { throw 'BitLocker ist auf dieser Edition nicht verfuegbar.' }
|
|
|
|
if ($c.action -eq 'disable') {
|
|
Disable-BitLocker -MountPoint $mount | Out-Null
|
|
Write-Log "Entschluesselung von $mount gestartet" 'OK'
|
|
return
|
|
}
|
|
if ($c.action -eq 'savekey') { Save-RecoveryKey $mount | Out-Null; return }
|
|
if ($c.action -ne 'enable') { return }
|
|
|
|
$tpm = Get-Tpm
|
|
if (-not $tpm.TpmPresent -or -not $tpm.TpmReady) { throw 'TPM nicht vorhanden oder nicht bereit.' }
|
|
|
|
if ($c.usePin) {
|
|
$fve = 'HKLM:\SOFTWARE\Policies\Microsoft\FVE'
|
|
Set-RegValue $fve 'UseAdvancedStartup' 1; Set-RegValue $fve 'EnableBDEWithNoTPM' 0
|
|
Set-RegValue $fve 'UseTPM' 2; Set-RegValue $fve 'UseTPMPIN' 2; Set-RegValue $fve 'UseTPMKey' 2; Set-RegValue $fve 'UseTPMKeyPIN' 2
|
|
if ($c.enhancedPin) { Set-RegValue $fve 'UseEnhancedPin' 1 }
|
|
}
|
|
$method = if ($c.method) { $c.method } else { 'XtsAes256' }
|
|
$vol = Get-BitLockerVolume -MountPoint $mount
|
|
|
|
if (-not ($vol.KeyProtector | Where-Object KeyProtectorType -eq 'RecoveryPassword')) {
|
|
Add-BitLockerKeyProtector -MountPoint $mount -RecoveryPasswordProtector | Out-Null
|
|
Write-Log 'Wiederherstellungskennwort-Schutz hinzugefuegt'
|
|
}
|
|
|
|
if ("$($vol.VolumeStatus)" -eq 'FullyDecrypted') {
|
|
$p = @{ MountPoint = $mount; EncryptionMethod = $method; ErrorAction = 'Stop' }
|
|
if (Test-True $c.usedSpaceOnly) { $p.UsedSpaceOnly = $true }
|
|
if (Test-True $c.skipHardwareTest) { $p.SkipHardwareTest = $true }
|
|
if ($c.usePin -and $c.pin) {
|
|
$p.TpmAndPinProtector = $true; $p.Pin = (ConvertTo-SecureString $c.pin -AsPlainText -Force)
|
|
} else { $p.TpmProtector = $true }
|
|
Enable-BitLocker @p | Out-Null
|
|
Write-Log "BitLocker auf $mount aktiviert ($method)" 'OK'
|
|
} else {
|
|
# z.B. Geraeteverschluesselung "Warten auf Aktivierung"
|
|
Write-Log "Volume-Status: $($vol.VolumeStatus) - ergaenze Schutzvorrichtungen und aktiviere Schutz"
|
|
$types = @($vol.KeyProtector | ForEach-Object { "$($_.KeyProtectorType)" })
|
|
if ($c.usePin -and $c.pin -and 'TpmPin' -notin $types) {
|
|
Add-BitLockerKeyProtector -MountPoint $mount -TpmAndPinProtector -Pin (ConvertTo-SecureString $c.pin -AsPlainText -Force) | Out-Null
|
|
} elseif (-not ($types | Where-Object { $_ -like 'Tpm*' })) {
|
|
Add-BitLockerKeyProtector -MountPoint $mount -TpmProtector | Out-Null
|
|
}
|
|
Resume-BitLocker -MountPoint $mount | Out-Null
|
|
Write-Log "Schutz auf $mount aktiv" 'OK'
|
|
}
|
|
|
|
$rp = Save-RecoveryKey $mount
|
|
foreach ($k in $rp) {
|
|
if (Test-True $c.backupAad) {
|
|
try { BackupToAAD-BitLockerKeyProtector -MountPoint $mount -KeyProtectorId $k.KeyProtectorId -ErrorAction Stop | Out-Null; Write-Log 'Schluessel in Entra ID gesichert' 'OK' }
|
|
catch { Write-Log "Entra-ID-Sicherung fehlgeschlagen: $_" 'WARN' }
|
|
}
|
|
if (Test-True $c.backupAd) {
|
|
try { Backup-BitLockerKeyProtector -MountPoint $mount -KeyProtectorId $k.KeyProtectorId -ErrorAction Stop | Out-Null; Write-Log 'Schluessel im AD gesichert' 'OK' }
|
|
catch { Write-Log "AD-Sicherung fehlgeschlagen: $_" 'WARN' }
|
|
}
|
|
}
|
|
if (-not (Test-True $c.skipHardwareTest) -and "$($vol.VolumeStatus)" -eq 'FullyDecrypted') {
|
|
Write-Log 'Hardwaretest aktiv: Verschluesselung startet nach dem naechsten Neustart.' 'WARN'
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Windows Update / Treiber ------------------------------------------------
|
|
|
|
$script:MUServiceId = '7971f918-a847-4430-9279-4a52d1efe18d'
|
|
|
|
function New-UpdateSearcher {
|
|
$session = New-Object -ComObject Microsoft.Update.Session
|
|
$session.ClientApplicationID = 'KDS Install Agent'
|
|
$searcher = $session.CreateUpdateSearcher()
|
|
try {
|
|
# Microsoft Update aktivieren (liefert Treiber + Office/andere MS-Produkte)
|
|
$sm = New-Object -ComObject Microsoft.Update.ServiceManager
|
|
$sm.AddService2($script:MUServiceId, 7, '') | Out-Null
|
|
$searcher.ServerSelection = 3
|
|
$searcher.ServiceID = $script:MUServiceId
|
|
} catch { Write-Log "Microsoft Update nicht registrierbar, nutze Windows Update: $_" 'WARN' }
|
|
return @{ Session = $session; Searcher = $searcher }
|
|
}
|
|
|
|
function Find-WindowsUpdates {
|
|
param($Data)
|
|
$types = if ($Data.type -eq 'all' -or -not $Data.type) { @('Software', 'Driver') } else { @($Data.type) }
|
|
$s = New-UpdateSearcher
|
|
$list = @()
|
|
foreach ($t in $types) {
|
|
Write-Log "Suche nach $(if($t -eq 'Driver'){'Treiber'}else{'Windows'})-Updates ..."
|
|
$r = $s.Searcher.Search("IsInstalled=0 and IsHidden=0 and Type='$t'")
|
|
foreach ($u in $r.Updates) {
|
|
$list += [pscustomobject]@{
|
|
id = $u.Identity.UpdateID
|
|
title = $u.Title
|
|
type = $t
|
|
kb = (@($u.KBArticleIDs) -join ',')
|
|
sizeMB = [math]::Round($u.MaxDownloadSize / 1MB, 1)
|
|
class = if ($t -eq 'Driver') { "$($u.DriverClass)" } else { (@($u.Categories | Select-Object -First 1 | ForEach-Object Name) -join '') }
|
|
}
|
|
}
|
|
Write-Log " $(@($r.Updates).Count) gefunden"
|
|
}
|
|
return , $list
|
|
}
|
|
|
|
function Install-WindowsUpdates {
|
|
param($Data)
|
|
$ids = @($Data.ids)
|
|
$s = New-UpdateSearcher
|
|
Write-Log 'Ermittle Updates ...'
|
|
$r = $s.Searcher.Search("IsInstalled=0 and IsHidden=0")
|
|
$coll = New-Object -ComObject Microsoft.Update.UpdateColl
|
|
foreach ($u in $r.Updates) {
|
|
if ($Data.all -or $u.Identity.UpdateID -in $ids) {
|
|
if ($Data.type -and $Data.type -ne 'all' -and $u.Type -ne $(if ($Data.type -eq 'Driver') { 2 } else { 1 })) { continue }
|
|
if (-not $u.EulaAccepted) { $u.AcceptEula() }
|
|
$coll.Add($u) | Out-Null
|
|
Write-Log " + $($u.Title)"
|
|
}
|
|
}
|
|
if ($coll.Count -eq 0) { Write-Log 'Keine passenden Updates.' 'OK'; return }
|
|
|
|
Write-Log "Lade $($coll.Count) Update(s) herunter ..."
|
|
$dl = $s.Session.CreateUpdateDownloader(); $dl.Updates = $coll
|
|
$dr = $dl.Download()
|
|
Write-Log "Download-Ergebnis: $($dr.ResultCode) (2=OK)"
|
|
|
|
$inst = New-Object -ComObject Microsoft.Update.UpdateColl
|
|
foreach ($u in $coll) { if ($u.IsDownloaded) { $inst.Add($u) | Out-Null } }
|
|
Write-Log "Installiere $($inst.Count) Update(s) ..."
|
|
$installer = $s.Session.CreateUpdateInstaller(); $installer.Updates = $inst
|
|
$ir = $installer.Install()
|
|
for ($i = 0; $i -lt $inst.Count; $i++) {
|
|
$rc = $ir.GetUpdateResult($i).ResultCode
|
|
Write-Log (" {0}: {1}" -f $inst.Item($i).Title, @{2 = 'OK'; 3 = 'OK mit Fehlern'; 4 = 'Fehlgeschlagen'; 5 = 'Abgebrochen' }[[int]$rc]) $(if ($rc -eq 2) { 'OK' } else { 'WARN' })
|
|
}
|
|
if ($ir.RebootRequired) { Write-Log 'NEUSTART ERFORDERLICH' 'WARN' }
|
|
return [pscustomobject]@{ rebootRequired = $ir.RebootRequired; result = $ir.ResultCode }
|
|
}
|
|
|
|
function Invoke-VendorDriverUpdate {
|
|
param($Data)
|
|
$man = (Get-CimInstance Win32_ComputerSystem).Manufacturer
|
|
Write-Log "Hersteller: $man"
|
|
if (-not (Install-Winget)) { throw 'winget nicht verfuegbar.' }
|
|
if ($man -match 'Dell') {
|
|
Invoke-Winget @('install', '--id', 'Dell.CommandUpdate', '-e', '--silent', '--accept-package-agreements', '--accept-source-agreements', '--disable-interactivity') | Out-Null
|
|
$cli = @("$env:ProgramFiles\Dell\CommandUpdate\dcu-cli.exe", "${env:ProgramFiles(x86)}\Dell\CommandUpdate\dcu-cli.exe") | Where-Object { Test-Path $_ } | Select-Object -First 1
|
|
if (-not $cli) { throw 'dcu-cli.exe nicht gefunden' }
|
|
Write-Log 'Dell Command Update: suche und installiere Treiber/BIOS/Firmware ...'
|
|
& $cli /applyUpdates -silent -reboot=disable -autoSuspendBitLocker=enable 2>&1 | ForEach-Object { Write-Log " $_" }
|
|
Write-Log "dcu-cli ExitCode $LASTEXITCODE (0=OK, 1/5=Neustart noetig, 500=keine Updates)" 'OK'
|
|
} elseif ($man -match 'Lenovo') {
|
|
Invoke-Winget @('install', '--id', 'Lenovo.SystemUpdate', '-e', '--silent', '--accept-package-agreements', '--accept-source-agreements', '--disable-interactivity') | Out-Null
|
|
$tvsu = "${env:ProgramFiles(x86)}\Lenovo\System Update\tvsu.exe"
|
|
if (-not (Test-Path $tvsu)) { throw 'tvsu.exe nicht gefunden' }
|
|
Write-Log 'Lenovo System Update: installiere Treiber ...'
|
|
$p = Start-Process $tvsu -ArgumentList '/CM -search A -action INSTALL -includerebootpackages 1,3,4 -noicons -noreboot -nolicense' -Wait -PassThru
|
|
Write-Log "tvsu ExitCode $($p.ExitCode)" 'OK'
|
|
} elseif ($man -match 'HP|Hewlett') {
|
|
Invoke-Winget @('install', '--id', 'HP.ImageAssistant', '-e', '--silent', '--accept-package-agreements', '--accept-source-agreements', '--disable-interactivity') | Out-Null
|
|
$hpia = Get-ChildItem "$env:ProgramFiles\HP\HPIA", "${env:ProgramFiles(x86)}\HP\HPIA", 'C:\SWSetup' -Filter HPImageAssistant.exe -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if (-not $hpia) { throw 'HPImageAssistant.exe nicht gefunden' }
|
|
$rep = Get-KdsDataDir 'logs\hpia'
|
|
Write-Log 'HP Image Assistant: analysiere und installiere ...'
|
|
$p = Start-Process $hpia.FullName -ArgumentList "/Operation:Analyze /Action:Install /Category:Drivers,Firmware /Selection:All /Silent /ReportFolder:`"$rep`" /SoftpaqDownloadFolder:`"$env:TEMP\hpia`"" -Wait -PassThru
|
|
Write-Log "HPIA ExitCode $($p.ExitCode) (0/256=OK, 3010=Neustart)" 'OK'
|
|
} else {
|
|
Write-Log 'Kein Hersteller-Tool hinterlegt - bitte Treiber ueber Windows Update installieren.' 'WARN'
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Bloatware ---------------------------------------------------------------
|
|
|
|
function Get-UninstallEntries {
|
|
$keys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
|
Get-ItemProperty $keys -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -and -not $_.SystemComponent -and -not $_.ParentKeyName }
|
|
}
|
|
|
|
function Get-BloatwareInventory {
|
|
# Liefert alle auf diesem Geraet gefundenen Bloatware-Kandidaten
|
|
$cat = [IO.File]::ReadAllText((Join-Path $global:KdsRoot 'config\bloatware.json')) | ConvertFrom-Json
|
|
$items = New-Object System.Collections.ArrayList
|
|
|
|
# 1) Store-/Appx-Apps (installiert fuer irgendeinen Benutzer oder provisioniert fuer neue Benutzer)
|
|
$installed = @(Get-AppxPackage -AllUsers -ErrorAction SilentlyContinue | Where-Object { -not $_.NonRemovable })
|
|
$provisioned = @(Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue)
|
|
foreach ($e in $cat.appx) {
|
|
$names = @($installed | Where-Object Name -like $e.pattern | ForEach-Object Name) + @($provisioned | Where-Object DisplayName -like $e.pattern | ForEach-Object DisplayName)
|
|
foreach ($n in ($names | Sort-Object -Unique)) {
|
|
if ($items | Where-Object id -eq "appx:$n") { continue }
|
|
[void]$items.Add([pscustomobject]@{
|
|
id = "appx:$n"; kind = 'appx'; name = $(if ($e.pattern -notmatch '\*') { $e.name } else { "$($e.name) ($n)" })
|
|
detail = $n; category = $e.category; recommended = [bool]$e.recommended
|
|
})
|
|
}
|
|
}
|
|
|
|
# 2) Klassische Programme + vorinstallierte Office-Versionen (Click-to-Run)
|
|
foreach ($u in Get-UninstallEntries) {
|
|
$dn = [string]$u.DisplayName
|
|
if ($u.UninstallString -match 'OfficeClickToRun\.exe') {
|
|
# Consumer-Vorinstallationen (Home/Personal/OneNote Free) empfehlen, Business-Versionen nur anzeigen
|
|
$consumer = $u.UninstallString -match 'productstoremove=(O365HomePrem|O365Personal|HomeStudent|HomeBusiness|Personal|OneNoteFree|Professional)Retail'
|
|
[void]$items.Add([pscustomobject]@{
|
|
id = "arp:$dn"; kind = 'win32'; name = $dn; detail = $(if ($u.UninstallString -match 'productstoremove=([^ ]+)') { $Matches[1] } else { 'Click-to-Run' })
|
|
category = 'Office / OneNote vorinstalliert'; recommended = $consumer
|
|
})
|
|
continue
|
|
}
|
|
if ($dn -match '^Microsoft OneDrive') { continue } # eigener Eintrag
|
|
$hit = $cat.win32 | Where-Object { $dn -match $_.pattern } | Select-Object -First 1
|
|
if ($hit) {
|
|
[void]$items.Add([pscustomobject]@{
|
|
id = "arp:$dn"; kind = 'win32'; name = $dn; detail = "$($u.Publisher) $($u.DisplayVersion)".Trim()
|
|
category = $hit.category; recommended = [bool]$hit.recommended
|
|
})
|
|
}
|
|
}
|
|
|
|
# 3) OneDrive (systemweit + Autostart fuer neue Benutzer)
|
|
if ((Get-OneDriveSetups) -or (Get-UninstallEntries | Where-Object DisplayName -match '^Microsoft OneDrive')) {
|
|
[void]$items.Add([pscustomobject]@{
|
|
id = 'onedrive'; kind = 'onedrive'; name = 'Microsoft OneDrive'; detail = 'Deinstallation + keine Einrichtung fuer neue Benutzer'
|
|
category = 'Office / OneNote vorinstalliert'; recommended = $false
|
|
})
|
|
}
|
|
return @($items)
|
|
}
|
|
|
|
function Get-OneDriveSetups {
|
|
@("$env:SystemRoot\System32\OneDriveSetup.exe", "$env:SystemRoot\SysWOW64\OneDriveSetup.exe") +
|
|
@(Get-ChildItem "$env:ProgramFiles\Microsoft OneDrive\*\OneDriveSetup.exe", "${env:ProgramFiles(x86)}\Microsoft OneDrive\*\OneDriveSetup.exe" -ErrorAction SilentlyContinue | ForEach-Object FullName) |
|
|
Where-Object { Test-Path $_ }
|
|
}
|
|
|
|
function Remove-OneDrive {
|
|
Get-Process OneDrive -ErrorAction SilentlyContinue | Stop-Process -Force
|
|
foreach ($s in Get-OneDriveSetups) {
|
|
$a = if ($s -like "$env:ProgramFiles*" -or $s -like "${env:ProgramFiles(x86)}*") { '/uninstall /allusers' } else { '/uninstall' }
|
|
Write-Log " $s $a"
|
|
Start-Process $s -ArgumentList $a -Wait -WindowStyle Hidden
|
|
}
|
|
# Automatische Einrichtung fuer neue Benutzerprofile verhindern
|
|
reg load HKU\KdsDefault "$env:SystemDrive\Users\Default\NTUSER.DAT" 2>$null | Out-Null
|
|
reg delete 'HKU\KdsDefault\Software\Microsoft\Windows\CurrentVersion\Run' /v OneDriveSetup /f 2>$null | Out-Null
|
|
[gc]::Collect(); reg unload HKU\KdsDefault 2>$null | Out-Null
|
|
Remove-Item "$env:PUBLIC\Desktop\OneDrive.lnk", "$env:ProgramData\Microsoft\Windows\Start Menu\Programs\OneDrive.lnk" -Force -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
function Remove-Win32App {
|
|
param([string]$DisplayName)
|
|
$u = Get-UninstallEntries | Where-Object DisplayName -eq $DisplayName | Select-Object -First 1
|
|
if (-not $u) { Write-Log ' nicht (mehr) installiert'; return 0 }
|
|
$us = [string]$u.UninstallString
|
|
if ($us -match 'OfficeClickToRun\.exe') {
|
|
# ARP-String von Click-to-Run + DisplayLevel=False = stille Deinstallation
|
|
if ($us -match '^"([^"]+)"\s*(.*)$') { $exe = $Matches[1]; $rest = $Matches[2] }
|
|
else { $exe, $rest = $us -split ' ', 2 }
|
|
return (Start-Process $exe -ArgumentList "$rest DisplayLevel=False" -Wait -PassThru).ExitCode
|
|
}
|
|
if ($us -match 'msiexec' -and $u.PSChildName -match '^\{[0-9A-F\-]+\}$') {
|
|
return (Start-Process msiexec.exe -ArgumentList "/x $($u.PSChildName) /qn /norestart" -Wait -PassThru).ExitCode
|
|
}
|
|
if ($u.QuietUninstallString) {
|
|
return (Start-Process cmd.exe -ArgumentList "/c `"$($u.QuietUninstallString)`"" -Wait -PassThru -WindowStyle Hidden).ExitCode
|
|
}
|
|
# Fallback: winget kennt fuer viele ARP-Eintraege den passenden Silent-Schalter
|
|
if (Get-WingetPath) {
|
|
$arch = if ($u.PSPath -match 'WOW6432Node') { 'X86' } else { 'X64' }
|
|
$code = Invoke-Winget @('uninstall', '--id', "ARP\Machine\$arch\$($u.PSChildName)", '--exact', '--silent', '--force', '--accept-source-agreements', '--disable-interactivity')
|
|
if ($code -eq 0) { return 0 }
|
|
}
|
|
Write-Log ' Kein stiller Deinstaller bekannt - starte Standard-Deinstallation (evtl. mit Dialog)' 'WARN'
|
|
return (Start-Process cmd.exe -ArgumentList "/c `"$us`"" -Wait -PassThru).ExitCode
|
|
}
|
|
|
|
function Remove-Bloatware {
|
|
param($Data)
|
|
$cfg = $Data.bloatware
|
|
Write-Log 'Ermittle installierte Bloatware ...'
|
|
$inv = Get-BloatwareInventory
|
|
$ids = @($cfg.ids)
|
|
$keep = @($cfg.keep)
|
|
$sel = @($inv | Where-Object { ($_.id -in $ids) -or ((Test-True $cfg.recommended) -and $_.recommended -and $_.id -notin $keep) })
|
|
if (-not $sel.Count) { Write-Log 'Nichts zu entfernen.' 'OK'; return }
|
|
Write-Log "$($sel.Count) Eintraege werden entfernt"
|
|
$fail = 0
|
|
foreach ($it in $sel) {
|
|
Write-Log "- $($it.name) [$($it.category)]"
|
|
try {
|
|
switch ($it.kind) {
|
|
'appx' {
|
|
$n = $it.detail
|
|
Get-AppxPackage -AllUsers -Name $n -ErrorAction SilentlyContinue | ForEach-Object {
|
|
try { Remove-AppxPackage -Package $_.PackageFullName -AllUsers -ErrorAction Stop } catch { Write-Log " $($_.Exception.Message)" 'WARN' }
|
|
}
|
|
Get-AppxProvisionedPackage -Online | Where-Object DisplayName -eq $n | ForEach-Object {
|
|
Remove-AppxProvisionedPackage -Online -PackageName $_.PackageName -AllUsers -ErrorAction SilentlyContinue | Out-Null
|
|
}
|
|
Write-Log ' entfernt' 'OK'
|
|
}
|
|
'win32' {
|
|
$code = Remove-Win32App ($it.id -replace '^arp:', '')
|
|
if ($code -in 0, 3010, 1605, 1641) { Write-Log " entfernt ($code)" 'OK' } else { throw "ExitCode $code" }
|
|
}
|
|
'onedrive' { Remove-OneDrive; Write-Log ' OneDrive entfernt' 'OK' }
|
|
}
|
|
} catch { $fail++; Write-Log " Fehler: $_" 'ERROR' }
|
|
}
|
|
Write-Log "Bloatware: $($sel.Count - $fail) entfernt, $fail Fehler" $(if ($fail) { 'WARN' } else { 'OK' })
|
|
if ($sel | Where-Object name -match 'McAfee') { Write-Log 'Tipp: McAfee-Reste ggf. mit dem McAfee Consumer Product Removal Tool (MCPR) entfernen.' 'WARN' }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region WLAN --------------------------------------------------------------------
|
|
|
|
function New-WifiProfileXml {
|
|
param($W)
|
|
$e = [Security.SecurityElement]::Escape([string]$W.ssid)
|
|
$hex = -join ([Text.Encoding]::UTF8.GetBytes([string]$W.ssid) | ForEach-Object { $_.ToString('X2') })
|
|
$auth = switch ($W.auth) { 'WPA3SAE' { 'WPA3SAE' } 'open' { 'open' } default { 'WPA2PSK' } }
|
|
$sec = if ($auth -eq 'open') {
|
|
'<authEncryption><authentication>open</authentication><encryption>none</encryption><useOneX>false</useOneX></authEncryption>'
|
|
} else {
|
|
if ("$($W.key)".Length -lt 8) { throw "WLAN '$($W.ssid)': Kennwort muss mindestens 8 Zeichen haben" }
|
|
"<authEncryption><authentication>$auth</authentication><encryption>AES</encryption><useOneX>false</useOneX></authEncryption>" +
|
|
"<sharedKey><keyType>passPhrase</keyType><protected>false</protected><keyMaterial>$([Security.SecurityElement]::Escape([string]$W.key))</keyMaterial></sharedKey>"
|
|
}
|
|
@"
|
|
<?xml version="1.0"?>
|
|
<WLANProfile xmlns="http://www.microsoft.com/networking/WLAN/profile/v1">
|
|
<name>$e</name>
|
|
<SSIDConfig><SSID><hex>$hex</hex><name>$e</name></SSID><nonBroadcast>$(if (Test-True $W.hidden) { 'true' } else { 'false' })</nonBroadcast></SSIDConfig>
|
|
<connectionType>ESS</connectionType>
|
|
<connectionMode>$(if ($W.auto -eq $false) { 'manual' } else { 'auto' })</connectionMode>
|
|
<MSM><security>$sec</security></MSM>
|
|
</WLANProfile>
|
|
"@
|
|
}
|
|
|
|
function Add-WifiProfiles {
|
|
param($Data)
|
|
$list = @($Data.wifi | Where-Object { $_.ssid })
|
|
if (-not $list.Count) { Write-Log 'Keine WLAN-Profile konfiguriert.' 'WARN'; return }
|
|
$tmp = Join-Path $env:TEMP "kds-wlan-$([guid]::NewGuid()).xml"
|
|
foreach ($w in $list) {
|
|
try {
|
|
[IO.File]::WriteAllText($tmp, (New-WifiProfileXml $w), (New-Object Text.UTF8Encoding($false)))
|
|
$out = netsh wlan add profile filename="$tmp" user=all 2>&1
|
|
if ($LASTEXITCODE -ne 0) { throw ($out -join ' ') }
|
|
Write-Log "WLAN '$($w.ssid)' gespeichert (alle Benutzer)" 'OK'
|
|
if (Test-True $w.connect) {
|
|
netsh wlan connect name="$($w.ssid)" | Out-Null
|
|
Write-Log " Verbindung zu '$($w.ssid)' angefordert"
|
|
}
|
|
} catch { Write-Log "WLAN '$($w.ssid)': $_" 'ERROR' }
|
|
finally { Remove-Item $tmp -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
}
|
|
|
|
function Get-WifiState {
|
|
$profiles = @(netsh wlan show profiles 2>$null | Where-Object { $_ -match '^\s{4}.+?\s:\s(.+)$' } | ForEach-Object { $Matches[1].Trim() })
|
|
$networks = @(netsh wlan show networks 2>$null | Where-Object { $_ -match '^SSID \d+ : (.*)$' } | ForEach-Object { $Matches[1].Trim() } | Where-Object { $_ } | Sort-Object -Unique)
|
|
$cur = netsh wlan show interfaces 2>$null | Where-Object { $_ -match '^\s+SSID\s+:\s(.+)$' } | ForEach-Object { $Matches[1].Trim() } | Select-Object -First 1
|
|
[pscustomobject]@{ profiles = $profiles; networks = $networks; connected = [string]$cur; hasWifi = [bool](Get-NetAdapter -Physical -ErrorAction SilentlyContinue | Where-Object { $_.NdisPhysicalMedium -eq 9 }) }
|
|
}
|
|
|
|
function Export-WifiProfiles {
|
|
# Liest die WLAN-Profile dieses Geraets inkl. Kennwort (fuer die Uebernahme in eine Blaupause)
|
|
$dir = Join-Path $env:TEMP "kds-wlan-export-$([guid]::NewGuid())"
|
|
New-Item $dir -ItemType Directory | Out-Null
|
|
try {
|
|
netsh wlan export profile key=clear folder="$dir" | Out-Null
|
|
@(Get-ChildItem $dir -Filter *.xml | ForEach-Object {
|
|
[xml]$x = [IO.File]::ReadAllText($_.FullName)
|
|
$sec = $x.WLANProfile.MSM.security
|
|
$a = [string]$sec.authEncryption.authentication
|
|
if ($a -notin 'WPA2PSK', 'WPA3SAE', 'open', 'WPAPSK') { return } # Enterprise-Profile ueberspringen
|
|
[pscustomobject]@{
|
|
ssid = [string]$x.WLANProfile.SSIDConfig.SSID.name
|
|
auth = $(if ($a -eq 'WPAPSK') { 'WPA2PSK' } else { $a })
|
|
key = [string]$sec.sharedKey.keyMaterial
|
|
hidden = ([string]$x.WLANProfile.SSIDConfig.nonBroadcast -eq 'true')
|
|
auto = ([string]$x.WLANProfile.connectionMode -ne 'manual')
|
|
}
|
|
})
|
|
} finally { Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region OpenVPN -----------------------------------------------------------------
|
|
|
|
function Get-KdsVpnDir { $d = Join-Path $env:ProgramData 'KDS-Agent\vpn'; New-Item $d -ItemType Directory -Force | Out-Null; $d }
|
|
|
|
function Get-PortableOvpnDir { Join-Path $global:KdsRoot 'tools\openvpn' }
|
|
|
|
function Get-OpenVpnExe {
|
|
# Portable Version vom Stick bevorzugen, sonst eine installierte
|
|
param([switch]$Installed)
|
|
$c = @("$env:ProgramFiles\OpenVPN\bin\openvpn.exe", "${env:ProgramFiles(x86)}\OpenVPN\bin\openvpn.exe")
|
|
if (-not $Installed) { $c = @((Join-Path (Get-PortableOvpnDir) 'bin\openvpn.exe')) + $c }
|
|
$c | Where-Object { Test-Path $_ } | Select-Object -First 1
|
|
}
|
|
|
|
function Get-OpenVpnPortableInfo {
|
|
$d = Get-PortableOvpnDir
|
|
[pscustomobject]@{
|
|
portable = Test-Path (Join-Path $d 'bin\openvpn.exe')
|
|
driver = Test-Path (Join-Path $d 'driver\OemVista.inf')
|
|
setup = [bool](Get-ChildItem (Join-Path $d 'setup') -Filter *.msi -ErrorAction SilentlyContinue)
|
|
version = [string](Get-Content (Join-Path $d 'version.txt') -ErrorAction SilentlyContinue)
|
|
installed = [bool](Get-OpenVpnExe -Installed)
|
|
}
|
|
}
|
|
|
|
function New-OpenVpnPortable {
|
|
# Erstellt tools\openvpn auf dem Stick aus den offiziellen Paketen (einmalig, braucht Internet)
|
|
param($Data)
|
|
$dst = Get-PortableOvpnDir
|
|
$tmp = Join-Path $env:TEMP "kds-ovpn-$([guid]::NewGuid())"
|
|
New-Item "$tmp\dl", "$dst\bin", "$dst\driver", "$dst\setup" -ItemType Directory -Force | Out-Null
|
|
try {
|
|
if (-not (Install-Winget)) { throw 'winget nicht verfuegbar' }
|
|
Write-Log 'Lade offizielles OpenVPN-Paket (winget) ...'
|
|
Invoke-Winget @('download', '--id', 'OpenVPNTechnologies.OpenVPN', '--exact', '--architecture', 'x64', '--download-directory', "$tmp\dl", '--accept-package-agreements', '--accept-source-agreements', '--disable-interactivity') | Out-Null
|
|
$msi = Get-ChildItem "$tmp\dl" -Filter *.msi | Select-Object -First 1
|
|
if (-not $msi) { throw 'OpenVPN-MSI konnte nicht geladen werden' }
|
|
Write-Log "Entpacke $($msi.Name) (ohne Installation) ..."
|
|
$p = Start-Process msiexec.exe -ArgumentList "/a `"$($msi.FullName)`" /qn TARGETDIR=`"$tmp\x`"" -Wait -PassThru
|
|
if ($p.ExitCode -ne 0) { throw "msiexec /a ExitCode $($p.ExitCode)" }
|
|
Copy-Item "$tmp\x\OpenVPN\bin\*" "$dst\bin" -Force
|
|
if (Test-Path "$tmp\x\OpenVPN\ssl") { Copy-Item "$tmp\x\OpenVPN\ssl" $dst -Recurse -Force }
|
|
Get-ChildItem "$dst\setup" -Filter *.msi | Remove-Item -Force
|
|
Copy-Item $msi.FullName "$dst\setup" -Force # fuer die dauerhafte Dienst-Installation (offline)
|
|
|
|
Write-Log 'Lade TAP-Windows6-Treiber (Microsoft-signiert) ...'
|
|
$rel = Invoke-RestMethod 'https://api.github.com/repos/OpenVPN/tap-windows6/releases/latest' -UseBasicParsing
|
|
$zip = $rel.assets | Where-Object name -eq 'dist.win10.zip' | Select-Object -First 1
|
|
if (-not $zip) { throw 'dist.win10.zip im TAP-Release nicht gefunden' }
|
|
Invoke-WebRequest $zip.browser_download_url -OutFile "$tmp\tap.zip" -UseBasicParsing
|
|
Expand-Archive "$tmp\tap.zip" "$tmp\tap" -Force
|
|
$amd = Get-ChildItem "$tmp\tap" -Recurse -Directory -Filter amd64 | Select-Object -First 1
|
|
Copy-Item "$($amd.FullName)\*" "$dst\driver" -Force
|
|
$ver = if ($msi.BaseName -match '_(\d+\.\d+\.\d+)_') { $Matches[1] } else { $msi.BaseName }
|
|
Set-Content "$dst\version.txt" "OpenVPN $ver, TAP $($rel.tag_name), erstellt $(Get-Date -Format 'dd.MM.yyyy')"
|
|
Write-Log "Portables OpenVPN erstellt: $dst" 'OK'
|
|
} finally { Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
|
|
function Get-TapAdapter {
|
|
Get-NetAdapter -IncludeHidden -ErrorAction SilentlyContinue | Where-Object InterfaceDescription -like 'TAP-Windows Adapter V9*' | Select-Object -First 1
|
|
}
|
|
|
|
function Initialize-PortableTap {
|
|
# Laedt den TAP-Treiber vom Stick nur, wenn noch kein TAP-Adapter existiert
|
|
$a = Get-TapAdapter
|
|
if ($a) { return $a.Name }
|
|
$drv = Join-Path (Get-PortableOvpnDir) 'driver'
|
|
if (-not (Test-Path "$drv\devcon.exe")) { throw 'TAP-Treiber fehlt in tools\openvpn\driver - zuerst "Portables OpenVPN erstellen"' }
|
|
Write-Log 'Lade TAP-Treiber vom Stick (temporaer) ...'
|
|
$p = Start-Process "$drv\devcon.exe" -ArgumentList "install `"$drv\OemVista.inf`" tap0901" -Wait -PassThru -WindowStyle Hidden
|
|
if ($p.ExitCode -notin 0, 1) { throw "TAP-Treiberinstallation fehlgeschlagen (devcon ExitCode $($p.ExitCode))" }
|
|
Set-Content (Join-Path (Get-KdsVpnDir) 'tap-created.txt') (Get-Date)
|
|
for ($i = 0; $i -lt 10 -and -not $a; $i++) { Start-Sleep 1; $a = Get-TapAdapter }
|
|
if (-not $a) { throw 'TAP-Adapter wurde nicht angelegt' }
|
|
Rename-NetAdapter -Name $a.Name -NewName 'KDS-VPN' -ErrorAction SilentlyContinue
|
|
Write-Log 'TAP-Adapter "KDS-VPN" bereit' 'OK'
|
|
return 'KDS-VPN'
|
|
}
|
|
|
|
function Remove-PortableTap {
|
|
# Entfernt nur einen vom Agent angelegten TAP-Adapter samt Treiber
|
|
$flag = Join-Path (Get-KdsVpnDir) 'tap-created.txt'
|
|
if (-not (Test-Path $flag)) { return }
|
|
$devcon = Join-Path (Get-PortableOvpnDir) 'driver\devcon.exe'
|
|
if (Test-Path $devcon) { Start-Process $devcon -ArgumentList 'remove tap0901' -Wait -WindowStyle Hidden | Out-Null }
|
|
Get-WindowsDriver -Online -ErrorAction SilentlyContinue | Where-Object { $_.OriginalFileName -match 'oemvista\.inf$' -and $_.ProviderName -match 'TAP' } | ForEach-Object {
|
|
pnputil /delete-driver $_.Driver /uninstall /force | Out-Null
|
|
}
|
|
Remove-Item $flag -Force
|
|
Write-Log 'Portabler TAP-Treiber wieder entfernt' 'OK'
|
|
}
|
|
|
|
function Install-OpenVpn {
|
|
# Dauerhafte Installation (nur fuer den Dienstbetrieb noetig) - bevorzugt offline vom Stick
|
|
$exe = Get-OpenVpnExe -Installed
|
|
if ($exe) { return $exe }
|
|
$msi = Get-ChildItem (Join-Path (Get-PortableOvpnDir) 'setup') -Filter *.msi -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($msi) {
|
|
Write-Log "Installiere OpenVPN vom Stick ($($msi.Name)) ..."
|
|
$p = Start-Process msiexec.exe -ArgumentList "/i `"$($msi.FullName)`" /qn /norestart" -Wait -PassThru
|
|
if ($p.ExitCode -notin 0, 3010) { throw "OpenVPN-MSI ExitCode $($p.ExitCode)" }
|
|
} else {
|
|
Write-Log 'Kein OpenVPN-Setup auf dem Stick - installiere ueber winget ...'
|
|
if (-not (Install-Winget)) { throw 'winget nicht verfuegbar' }
|
|
Invoke-Winget @('install', '--id', 'OpenVPNTechnologies.OpenVPN', '--exact', '--silent', '--scope', 'machine', '--accept-package-agreements', '--accept-source-agreements', '--disable-interactivity') | Out-Null
|
|
}
|
|
$exe = Get-OpenVpnExe -Installed
|
|
if (-not $exe) { throw 'OpenVPN-Installation fehlgeschlagen' }
|
|
Write-Log 'OpenVPN installiert' 'OK'
|
|
return $exe
|
|
}
|
|
|
|
function Get-OpenVpnProcesses {
|
|
@(Get-CimInstance Win32_Process -Filter "Name='openvpn.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -match 'kds-openvpn' })
|
|
}
|
|
|
|
function Get-VpnStatus {
|
|
$log = Join-Path (Get-KdsVpnDir) 'kds-openvpn.log'
|
|
$procs = Get-OpenVpnProcesses
|
|
$tail = if (Test-Path $log) { @(Get-Content $log -Tail 12 -ErrorAction SilentlyContinue | ForEach-Object { [string]$_ }) } else { @() }
|
|
$connected = $procs.Count -gt 0 -and (Test-Path $log) -and (Select-String -Path $log -Pattern 'Initialization Sequence Completed' -Quiet)
|
|
# nur den vom Agent genutzten Adapter auswerten (andere VPN-Clients ignorieren)
|
|
$devFile = Join-Path (Get-KdsVpnDir) 'kds-dev.txt'
|
|
$ip = @()
|
|
if ($connected -and (Test-Path $devFile)) {
|
|
$ip = @(Get-NetIPAddress -InterfaceAlias ([string](Get-Content $devFile)) -AddressFamily IPv4 -ErrorAction SilentlyContinue | ForEach-Object { [string]$_.IPAddress })
|
|
}
|
|
$inst = Get-OpenVpnExe -Installed
|
|
$kdsSvc = $inst -and [bool](Get-ChildItem (Join-Path (Split-Path (Split-Path $inst)) 'config-auto') -Filter 'kds-*.ovpn' -ErrorAction SilentlyContinue)
|
|
$svc = Get-Service OpenVPNService -ErrorAction SilentlyContinue
|
|
[pscustomobject]@{
|
|
installed = [bool]$inst; running = $procs.Count -gt 0; connected = [bool]$connected; ip = ($ip -join ', ')
|
|
kdsService = $kdsSvc; service = $(if ($kdsSvc -and $svc) { "$($svc.Status) / $($svc.StartType)" } else { '' }); log = ($tail -join "`n")
|
|
}
|
|
}
|
|
|
|
function Get-VpnAdapters {
|
|
@(Get-NetAdapter -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq 'Up' -and $_.InterfaceDescription -match 'TAP-Windows|Wintun|OpenVPN|Data Channel Offload' })
|
|
}
|
|
|
|
function Get-OvpnPath {
|
|
param([string]$Name)
|
|
if (-not $Name) { throw 'Keine VPN-Konfiguration (.ovpn) ausgewaehlt' }
|
|
$p = Join-Path $global:KdsRoot "vpn\$(Split-Path $Name -Leaf)"
|
|
if (-not (Test-Path $p)) { throw "VPN-Konfiguration nicht gefunden: $p" }
|
|
return $p
|
|
}
|
|
|
|
function Write-VpnAuthFile {
|
|
param([string]$Path, $V)
|
|
Set-Content -Path $Path -Value @($V.user, $V.password) -Encoding ASCII
|
|
# nur SYSTEM und Administratoren duerfen die Datei lesen
|
|
icacls "$Path" /inheritance:r /grant:r '*S-1-5-18:F' '*S-1-5-32-544:F' | Out-Null
|
|
}
|
|
|
|
function Connect-KdsVpn {
|
|
param($Data)
|
|
$v = $Data.vpn
|
|
$cfg = Get-OvpnPath $v.config
|
|
if ((Get-VpnStatus).connected) { Write-Log 'VPN ist bereits verbunden.' 'OK'; return }
|
|
Disconnect-KdsVpn | Out-Null
|
|
$exe = Get-OpenVpnExe
|
|
if (-not $exe) { throw 'Kein OpenVPN gefunden - im Tab "VPN & Domaene" zuerst "Portables OpenVPN erstellen"' }
|
|
Write-Log "OpenVPN: $exe"
|
|
$dev = Initialize-PortableTap
|
|
Set-Content (Join-Path (Get-KdsVpnDir) 'kds-dev.txt') $dev
|
|
|
|
$dir = Get-KdsVpnDir
|
|
$log = Join-Path $dir 'kds-openvpn.log'
|
|
$auth = Join-Path $dir 'kds-openvpn-auth.txt'
|
|
$a = @('--config', "`"$cfg`"", '--cd', "`"$(Split-Path $cfg)`"", '--log', "`"$log`"", '--verb', '3', '--setenv', 'KDS', 'kds-openvpn',
|
|
'--disable-dco', '--dev-node', "`"$dev`"") # TAP-Adapter statt DCO (DCO-Treiber waere nicht portabel)
|
|
if ($v.user) { Write-VpnAuthFile $auth $v; $a += '--auth-user-pass', "`"$auth`"" }
|
|
Write-Log "Starte OpenVPN mit $(Split-Path $cfg -Leaf) ..."
|
|
$p = Start-Process $exe -ArgumentList $a -WindowStyle Hidden -PassThru
|
|
|
|
$deadline = (Get-Date).AddSeconds(90)
|
|
while ((Get-Date) -lt $deadline) {
|
|
Start-Sleep 2
|
|
$txt = if (Test-Path $log) { Get-Content $log -Raw -ErrorAction SilentlyContinue } else { '' }
|
|
if ($txt -match 'Initialization Sequence Completed') { break }
|
|
if ($txt -match 'AUTH_FAILED') { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue; throw 'VPN-Anmeldung fehlgeschlagen (AUTH_FAILED) - Benutzer/Kennwort pruefen' }
|
|
if ($p.HasExited) { throw "OpenVPN wurde beendet. Log:`n$((Get-Content $log -Tail 15 -ErrorAction SilentlyContinue) -join "`n")" }
|
|
}
|
|
if (-not ((Get-Content $log -Raw -ErrorAction SilentlyContinue) -match 'Initialization Sequence Completed')) {
|
|
Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue
|
|
throw 'Zeitueberschreitung beim VPN-Aufbau (90 s)'
|
|
}
|
|
Start-Sleep 3
|
|
if ($v.dns) {
|
|
$servers = @($v.dns -split '[,; ]+' | Where-Object { $_ })
|
|
Get-VpnAdapters | ForEach-Object { Set-DnsClientServerAddress -InterfaceIndex $_.ifIndex -ServerAddresses $servers }
|
|
Write-Log "DNS-Server am VPN-Adapter gesetzt: $($servers -join ', ')"
|
|
}
|
|
Write-Log "VPN verbunden ($((Get-VpnStatus).ip))" 'OK'
|
|
}
|
|
|
|
function Disconnect-KdsVpn {
|
|
$procs = Get-OpenVpnProcesses
|
|
$procs | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
|
|
Remove-Item (Join-Path (Get-KdsVpnDir) 'kds-openvpn-auth.txt') -Force -ErrorAction SilentlyContinue
|
|
if ($procs.Count) { Write-Log 'VPN getrennt' 'OK' }
|
|
return $procs.Count
|
|
}
|
|
|
|
function Install-KdsVpnService {
|
|
# VPN dauerhaft ueber den OpenVPN-Dienst (config-auto) - noetig, damit die erste Domaenenanmeldung den DC erreicht
|
|
param($Data)
|
|
$v = $Data.vpn
|
|
$cfg = Get-OvpnPath $v.config
|
|
Disconnect-KdsVpn | Out-Null
|
|
Remove-PortableTap
|
|
$exe = Install-OpenVpn
|
|
$auto = Join-Path (Split-Path (Split-Path $exe)) 'config-auto'
|
|
New-Item $auto -ItemType Directory -Force | Out-Null
|
|
Get-ChildItem $auto -Filter 'kds-*' | Remove-Item -Force
|
|
|
|
$lines = @(Get-Content $cfg | Where-Object { $_ -notmatch '^\s*auth-user-pass\b' })
|
|
# referenzierte Zertifikats-/Schluesseldateien mitkopieren
|
|
foreach ($l in $lines) {
|
|
if ($l -match '^\s*(ca|cert|key|tls-auth|tls-crypt|tls-crypt-v2|pkcs12|crl-verify|dh)\s+"?([^"\s]+)"?') {
|
|
$src = Join-Path (Split-Path $cfg) $Matches[2]
|
|
if (Test-Path $src) { Copy-Item $src $auto -Force }
|
|
}
|
|
}
|
|
if ($v.user) {
|
|
Write-VpnAuthFile (Join-Path $auto 'kds-auth.txt') $v
|
|
$lines += 'auth-user-pass kds-auth.txt'
|
|
}
|
|
$name = 'kds-' + ([IO.Path]::GetFileNameWithoutExtension($cfg) -replace '[^\w\-]', '_') + '.ovpn'
|
|
Set-Content -Path (Join-Path $auto $name) -Value $lines -Encoding ASCII
|
|
icacls "$auto" /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' | Out-Null
|
|
|
|
Set-Service OpenVPNService -StartupType Automatic
|
|
Restart-Service OpenVPNService
|
|
Write-Log "VPN als Dienst eingerichtet ($name, startet automatisch vor der Anmeldung)" 'OK'
|
|
}
|
|
|
|
function Remove-KdsVpnService {
|
|
$exe = Get-OpenVpnExe -Installed
|
|
if ($exe) { Get-ChildItem (Join-Path (Split-Path (Split-Path $exe)) 'config-auto') -Filter 'kds-*' -ErrorAction SilentlyContinue | Remove-Item -Force }
|
|
Restart-Service OpenVPNService -ErrorAction SilentlyContinue
|
|
Write-Log 'VPN-Dienstkonfiguration entfernt' 'OK'
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Domaene -----------------------------------------------------------------
|
|
|
|
function Test-KdsDomain {
|
|
param([string]$Name)
|
|
$r = [ordered]@{ domain = $Name; dns = $false; dcs = @(); ldap = $false; joined = ((Get-CimInstance Win32_ComputerSystem).Domain -eq $Name) }
|
|
try {
|
|
$srv = @(Resolve-DnsName "_ldap._tcp.dc._msdcs.$Name" -Type SRV -DnsOnly -ErrorAction Stop | Where-Object Type -eq 'SRV')
|
|
$r.dns = $srv.Count -gt 0
|
|
$r.dcs = @($srv | ForEach-Object { [string]$_.NameTarget })
|
|
if ($r.dcs) {
|
|
$c = New-Object Net.Sockets.TcpClient
|
|
$r.ldap = $c.ConnectAsync($r.dcs[0], 389).Wait(3000) -and $c.Connected
|
|
$c.Close()
|
|
}
|
|
} catch { $r.error = $_.Exception.Message }
|
|
[pscustomobject]$r
|
|
}
|
|
|
|
function Join-KdsDomain {
|
|
param($Data)
|
|
$d = $Data.domain
|
|
if (-not $d.name) { throw 'Keine Domaene angegeben' }
|
|
if ((Get-CimInstance Win32_ComputerSystem).Domain -eq $d.name) { Write-Log "Bereits Mitglied von $($d.name)" 'OK'; return }
|
|
|
|
if (Test-True $d.useVpn) { Connect-KdsVpn $Data }
|
|
|
|
Write-Log "Pruefe Erreichbarkeit von $($d.name) ..."
|
|
$t = Test-KdsDomain $d.name
|
|
if (-not $t.dns) { throw "Domaenencontroller nicht per DNS auffindbar (_ldap._tcp.dc._msdcs.$($d.name)). VPN/DNS pruefen. $($t.error)" }
|
|
Write-Log " DCs: $($t.dcs -join ', ') - LDAP erreichbar: $($t.ldap)"
|
|
|
|
$user = [string]$d.user
|
|
if ($user -notmatch '[\\@]') { $user = "$($d.name)\$user" }
|
|
$cred = New-Object PSCredential($user, (ConvertTo-SecureString ([string]$d.password) -AsPlainText -Force))
|
|
$p = @{ DomainName = $d.name; Credential = $cred; Force = $true; ErrorAction = 'Stop' }
|
|
if ($d.ou) { $p.OUPath = $d.ou }
|
|
$newName = $Data.system.computerName
|
|
if ($newName -and $newName -ne $env:COMPUTERNAME) { $p.NewName = $newName; $p.Options = 'JoinWithNewName', 'AccountCreate' }
|
|
|
|
Write-Log "Trete Domaene $($d.name) bei$(if ($d.ou) { " (OU: $($d.ou))" })$(if ($p.NewName) { " als $($p.NewName)" }) ..."
|
|
Add-Computer @p
|
|
Write-Log "Domaenenbeitritt erfolgreich - Neustart erforderlich" 'OK'
|
|
|
|
# Domaenengruppen/-benutzer zu lokalen Administratoren
|
|
$admins = @("$($d.admins)" -split '[,;\r\n]+' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
|
if ($admins.Count) {
|
|
$grp = (Get-LocalGroup -SID 'S-1-5-32-544').Name
|
|
$pending = @()
|
|
foreach ($a in $admins) {
|
|
$m = if ($a -match '\\') { $a } else { "$($d.name.Split('.')[0])\$a" }
|
|
net localgroup "$grp" "$m" /add 2>&1 | Out-Null
|
|
if ($LASTEXITCODE -eq 0) { Write-Log " $m -> lokale Administratoren" 'OK' } else { $pending += $m }
|
|
}
|
|
if ($pending) {
|
|
# nach dem Neustart per einmaliger Aufgabe nachholen
|
|
$cmd = ($pending | ForEach-Object { "net localgroup `"$grp`" `"$_`" /add" }) -join ' & '
|
|
schtasks /Create /TN 'KDS-LocalAdmins' /SC ONSTART /RU SYSTEM /RL HIGHEST /F /TR "cmd /c $cmd & schtasks /Delete /TN KDS-LocalAdmins /F" | Out-Null
|
|
Write-Log " Lokale Admins werden nach dem Neustart gesetzt: $($pending -join ', ')"
|
|
}
|
|
}
|
|
|
|
if ((Test-True $d.useVpn) -and (Test-True $d.vpnService)) { Install-KdsVpnService $Data }
|
|
if (Test-True $d.reboot) { $global:KdsRebootAfter = $true }
|
|
if ((Test-True $d.reboot) -and -not $global:KdsInProfile) {
|
|
shutdown /r /t 60 /c "KDS Install Agent: Neustart nach Domaenenbeitritt in 60 Sekunden"
|
|
Write-Log 'Neustart in 60 Sekunden (abbrechen: shutdown /a)' 'WARN'
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Gesamtprofil ------------------------------------------------------------
|
|
|
|
function Invoke-FullProfile {
|
|
param($Data)
|
|
$steps = @($Data.steps)
|
|
Write-Log "Starte Profil '$($Data.name)' mit Schritten: $($steps -join ', ')"
|
|
$run = {
|
|
param($title, $block)
|
|
Write-Log "===== $title ====="
|
|
try { & $block } catch { Write-Log "$title fehlgeschlagen: $_" 'ERROR' }
|
|
}
|
|
$global:KdsInProfile = $true
|
|
if ('system' -in $steps) { & $run 'System' { Set-SystemConfig $Data } }
|
|
if ('wifi' -in $steps) { & $run 'WLAN' { Add-WifiProfiles $Data } }
|
|
if ('bloatware' -in $steps) { & $run 'Bloatware entfernen' { Remove-Bloatware $Data } }
|
|
if ('security' -in $steps) { & $run 'Sicherheit' { Set-SecurityConfig $Data } }
|
|
if ('power' -in $steps) { & $run 'Energie' { Set-PowerConfig $Data } }
|
|
if ('apps' -in $steps) { & $run 'Programme' { Install-Apps $Data } }
|
|
if ('office' -in $steps) { & $run 'Office' { Install-Office ([pscustomobject]@{ office = $Data.office; mode = $Data.officeMode; sourcePath = $Data.officeSource; name = $Data.name }) } }
|
|
if ('shortcuts' -in $steps) { & $run 'Desktop-Verknuepfungen' { Set-DesktopShortcuts $Data } }
|
|
if ('drivers' -in $steps) { & $run 'Hersteller-Treiber' { Invoke-VendorDriverUpdate $Data } }
|
|
if ('updates' -in $steps) { & $run 'Windows Update' { Install-WindowsUpdates ([pscustomobject]@{ all = $true; type = 'all' }) | Out-Null } }
|
|
if ('domain' -in $steps) { & $run 'VPN & Domaenenbeitritt' { Join-KdsDomain $Data } }
|
|
if ('bitlocker' -in $steps) { & $run 'BitLocker' { Set-BitLockerConfig $Data } }
|
|
# Portables VPN wieder abbauen (ausser es laeuft als Dienst weiter)
|
|
if ((Get-OpenVpnProcesses).Count -or (Test-Path (Join-Path (Get-KdsVpnDir) 'tap-created.txt'))) {
|
|
& $run 'VPN trennen' { Disconnect-KdsVpn | Out-Null; Remove-PortableTap }
|
|
}
|
|
Write-Log 'Profil abgeschlossen.' 'OK'
|
|
if ($global:KdsRebootAfter) {
|
|
shutdown /r /t 120 /c "KDS Install Agent: Einrichtung abgeschlossen - Neustart in 2 Minuten"
|
|
Write-Log 'Neustart in 2 Minuten (abbrechen: shutdown /a)' 'WARN'
|
|
}
|
|
}
|
|
|
|
#endregion
|