From 30d930c667ba8ef1f5c84527db9000ea3078cfac Mon Sep 17 00:00:00 2001 From: X9 Date: Sat, 14 Mar 2026 09:44:38 +0100 Subject: [PATCH] Implement full deployment script suite (steps 1-7) - Deploy-Windows.ps1: master script with Write-Log, Invoke-Step, summary report, DryRun support - 01-bloatware.ps1: remove AppX packages, Windows Capabilities, Optional Features - 02-software.ps1: winget installs from config.json, set Adobe Reader as default PDF app - 03-system-registry.ps1: HKLM tweaks (NRO bypass, Teams, Widgets, Edge, OneDrive, GameDVR, Recall, timezone) - 04-default-profile.ps1: NTUSER.DAT changes for taskbar, Explorer, Start menu, NumLock, Copilot - 05-personalization.ps1: dark/light theme, accent color #223B47, transparency off, wallpaper - 06-scheduled-tasks.ps1: ShowAllTrayIcons, PDF-DefaultApp, UnlockStartLayout tasks - 07-desktop-info.ps1: DesktopInfo render script (System.Drawing BMP), scheduled task, deploy date registry - tests/Test-Deployment.ps1: post-deployment verification, 30+ checks - CLAUDE.md: add Czech communication preference Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 7 + Deploy-Windows.ps1 | 198 +++++++++++++++++++++++++- scripts/01-bloatware.ps1 | 185 ++++++++++++++++++++++++- scripts/02-software.ps1 | 123 ++++++++++++++++- scripts/03-system-registry.ps1 | 180 +++++++++++++++++++++++- scripts/04-default-profile.ps1 | 245 ++++++++++++++++++++++++++++++++- scripts/05-personalization.ps1 | 167 +++++++++++++++++++++- scripts/06-scheduled-tasks.ps1 | 162 +++++++++++++++++++++- scripts/07-desktop-info.ps1 | 218 ++++++++++++++++++++++++++++- tests/Test-Deployment.ps1 | 223 +++++++++++++++++++++++++++++- 10 files changed, 1698 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f4518a0..c0a31ea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,6 +38,13 @@ windows-deployment/ --- +## Communication + +- Communicate with the user in Czech +- Code, comments, log messages: English only (no diacritics rule still applies) + +--- + ## Conventions and rules ### PowerShell diff --git a/Deploy-Windows.ps1 b/Deploy-Windows.ps1 index f953d26..512845a 100644 --- a/Deploy-Windows.ps1 +++ b/Deploy-Windows.ps1 @@ -1,2 +1,196 @@ -# TODO: master deployment script -# See SPEC.md for full specification +#Requires -RunAsAdministrator + +[CmdletBinding()] +param( + [switch]$SkipBloatware, + [switch]$SkipSoftware, + [switch]$SkipDefaultProfile, + [switch]$DryRun +) + +$ErrorActionPreference = "Continue" + +# ----------------------------------------------------------------------- +# Paths +# ----------------------------------------------------------------------- +$ScriptRoot = $PSScriptRoot +$LogDir = "C:\Windows\Setup\Scripts" +$LogFile = "$LogDir\Deploy.log" +$ConfigFile = "$ScriptRoot\config\config.json" + +# ----------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------- +function Write-Log { + param( + [string]$Message, + [ValidateSet("INFO","OK","ERROR","WARN","STEP")] + [string]$Level = "INFO" + ) + $timestamp = Get-Date -Format "HH:mm:ss" + $line = "[$timestamp] [$Level] $Message" + Add-Content -Path $LogFile -Value $line -Encoding UTF8 + switch ($Level) { + "OK" { Write-Host $line -ForegroundColor Green } + "ERROR" { Write-Host $line -ForegroundColor Red } + "WARN" { Write-Host $line -ForegroundColor Yellow } + "STEP" { Write-Host $line -ForegroundColor Cyan } + default { Write-Host $line } + } +} + +# ----------------------------------------------------------------------- +# Step runner - catches errors, logs, always continues +# ----------------------------------------------------------------------- +$StepResults = [System.Collections.Generic.List[hashtable]]::new() + +function Invoke-Step { + param( + [string]$Name, + [scriptblock]$Action + ) + + Write-Log "---- $Name ----" -Level STEP + + if ($DryRun) { + Write-Log "DryRun - skipping execution" -Level WARN + $StepResults.Add(@{ Name = $Name; Status = "DRYRUN" }) + return + } + + try { + & $Action + Write-Log "$Name - OK" -Level OK + $StepResults.Add(@{ Name = $Name; Status = "OK" }) + } + catch { + Write-Log "$Name - ERROR: $_" -Level ERROR + $StepResults.Add(@{ Name = $Name; Status = "ERROR" }) + } +} + +# ----------------------------------------------------------------------- +# Init +# ----------------------------------------------------------------------- +if (-not (Test-Path $LogDir)) { + New-Item -ItemType Directory -Path $LogDir -Force | Out-Null +} + +Write-Log "========================================" -Level INFO +Write-Log "Deploy-Windows.ps1 started" -Level INFO +Write-Log "Computer: $env:COMPUTERNAME" -Level INFO +Write-Log "User: $env:USERNAME" -Level INFO +Write-Log "Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -Level INFO +if ($DryRun) { Write-Log "Mode: DRY RUN" -Level WARN } +Write-Log "========================================" -Level INFO + +# ----------------------------------------------------------------------- +# Load config +# ----------------------------------------------------------------------- +$Config = $null +Invoke-Step -Name "Load config.json" -Action { + if (-not (Test-Path $ConfigFile)) { + throw "config.json not found: $ConfigFile" + } + $script:Config = Get-Content $ConfigFile -Raw -Encoding UTF8 | ConvertFrom-Json + Write-Log "Config loaded from $ConfigFile" -Level INFO +} + +# ----------------------------------------------------------------------- +# Step 1 - Bloatware removal +# ----------------------------------------------------------------------- +if ($SkipBloatware) { + Write-Log "Step 1 - Bloatware removal: SKIPPED (-SkipBloatware)" -Level WARN + $StepResults.Add(@{ Name = "Step 1 - Bloatware removal"; Status = "SKIPPED" }) +} else { + Invoke-Step -Name "Step 1 - Bloatware removal" -Action { + & "$ScriptRoot\scripts\01-bloatware.ps1" -Config $Config -LogFile $LogFile + } +} + +# ----------------------------------------------------------------------- +# Step 2 - Software installation +# ----------------------------------------------------------------------- +if ($SkipSoftware) { + Write-Log "Step 2 - Software installation: SKIPPED (-SkipSoftware)" -Level WARN + $StepResults.Add(@{ Name = "Step 2 - Software installation"; Status = "SKIPPED" }) +} else { + Invoke-Step -Name "Step 2 - Software installation" -Action { + & "$ScriptRoot\scripts\02-software.ps1" -Config $Config -LogFile $LogFile + } +} + +# ----------------------------------------------------------------------- +# Step 3 - System registry (HKLM) +# ----------------------------------------------------------------------- +Invoke-Step -Name "Step 3 - System registry" -Action { + & "$ScriptRoot\scripts\03-system-registry.ps1" -Config $Config -LogFile $LogFile +} + +# ----------------------------------------------------------------------- +# Step 4 - Default profile (NTUSER.DAT) +# ----------------------------------------------------------------------- +if ($SkipDefaultProfile) { + Write-Log "Step 4 - Default profile: SKIPPED (-SkipDefaultProfile)" -Level WARN + $StepResults.Add(@{ Name = "Step 4 - Default profile"; Status = "SKIPPED" }) +} else { + Invoke-Step -Name "Step 4 - Default profile" -Action { + & "$ScriptRoot\scripts\04-default-profile.ps1" -Config $Config -LogFile $LogFile + } +} + +# ----------------------------------------------------------------------- +# Step 5 - Personalization +# ----------------------------------------------------------------------- +Invoke-Step -Name "Step 5 - Personalization" -Action { + & "$ScriptRoot\scripts\05-personalization.ps1" -Config $Config -LogFile $LogFile +} + +# ----------------------------------------------------------------------- +# Step 6 - Scheduled tasks +# ----------------------------------------------------------------------- +Invoke-Step -Name "Step 6 - Scheduled tasks" -Action { + & "$ScriptRoot\scripts\06-scheduled-tasks.ps1" -Config $Config -LogFile $LogFile +} + +# ----------------------------------------------------------------------- +# Step 7 - DesktopInfo +# ----------------------------------------------------------------------- +Invoke-Step -Name "Step 7 - DesktopInfo" -Action { + & "$ScriptRoot\scripts\07-desktop-info.ps1" -Config $Config -LogFile $LogFile +} + +# ----------------------------------------------------------------------- +# Summary +# ----------------------------------------------------------------------- +Write-Log "========================================" -Level INFO +Write-Log "SUMMARY" -Level INFO +Write-Log "========================================" -Level INFO + +$countOK = ($StepResults | Where-Object { $_.Status -eq "OK" }).Count +$countError = ($StepResults | Where-Object { $_.Status -eq "ERROR" }).Count +$countSkipped = ($StepResults | Where-Object { $_.Status -eq "SKIPPED" }).Count +$countDryRun = ($StepResults | Where-Object { $_.Status -eq "DRYRUN" }).Count + +foreach ($r in $StepResults) { + $lvl = switch ($r.Status) { + "OK" { "OK" } + "ERROR" { "ERROR" } + "SKIPPED" { "WARN" } + "DRYRUN" { "WARN" } + } + Write-Log "$($r.Status.PadRight(8)) $($r.Name)" -Level $lvl +} + +Write-Log "----------------------------------------" -Level INFO +Write-Log "OK: $countOK ERROR: $countError SKIPPED: $countSkipped DRYRUN: $countDryRun" -Level INFO +Write-Log "Log saved to: $LogFile" -Level INFO +Write-Log "========================================" -Level INFO + +if ($countError -gt 0) { + Write-Log "Deployment finished with errors. Review log: $LogFile" -Level ERROR + exit 1 +} else { + Write-Log "Deployment finished successfully." -Level OK + exit 0 +} diff --git a/scripts/01-bloatware.ps1 b/scripts/01-bloatware.ps1 index a78d201..d0f311a 100644 --- a/scripts/01-bloatware.ps1 +++ b/scripts/01-bloatware.ps1 @@ -1 +1,184 @@ -# TODO: 01-bloatware.ps1 +param( + [object]$Config, + [string]$LogFile +) + +$ErrorActionPreference = "Continue" + +function Write-Log { + param([string]$Message, [string]$Level = "INFO") + $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" + Add-Content -Path $LogFile -Value $line -Encoding UTF8 +} + +# ----------------------------------------------------------------------- +# 1a - AppX packages +# ----------------------------------------------------------------------- +$AppxToRemove = @( + "Microsoft.Microsoft3DViewer" + "Microsoft.BingSearch" + "Microsoft.WindowsCamera" + "Clipchamp.Clipchamp" + "Microsoft.WindowsAlarms" + "Microsoft.Copilot" + "Microsoft.549981C3F5F10" + "Microsoft.Windows.DevHome" + "MicrosoftCorporationII.MicrosoftFamily" + "Microsoft.WindowsFeedbackHub" + "Microsoft.Edge.GameAssist" + "Microsoft.GetHelp" + "Microsoft.Getstarted" + "microsoft.windowscommunicationsapps" + "Microsoft.WindowsMaps" + "Microsoft.MixedReality.Portal" + "Microsoft.BingNews" + "Microsoft.MicrosoftOfficeHub" + "Microsoft.Office.OneNote" + "Microsoft.OutlookForWindows" + "Microsoft.Paint" + "Microsoft.MSPaint" + "Microsoft.People" + "Microsoft.Windows.Photos" + "Microsoft.PowerAutomateDesktop" + "MicrosoftCorporationII.QuickAssist" + "Microsoft.SkypeApp" + "Microsoft.ScreenSketch" + "Microsoft.MicrosoftSolitaireCollection" + "Microsoft.MicrosoftStickyNotes" + "MicrosoftTeams" + "MSTeams" + "Microsoft.Todos" + "Microsoft.WindowsSoundRecorder" + "Microsoft.Wallet" + "Microsoft.BingWeather" + "Microsoft.WindowsTerminal" + "Microsoft.Xbox.TCUI" + "Microsoft.XboxApp" + "Microsoft.XboxGameOverlay" + "Microsoft.XboxGamingOverlay" + "Microsoft.XboxIdentityProvider" + "Microsoft.XboxSpeechToTextOverlay" + "Microsoft.GamingApp" + "Microsoft.YourPhone" + "Microsoft.ZuneMusic" + "Microsoft.ZuneVideo" +) + +# Packages to always keep +$KeepPackages = @("Microsoft.WindowsCalculator") +if ($Config -and $Config.bloatware -and $Config.bloatware.keepPackages) { + $KeepPackages += $Config.bloatware.keepPackages +} +$KeepPackages = $KeepPackages | Select-Object -Unique + +Write-Log "1a - Removing AppX packages" -Level STEP + +foreach ($pkg in $AppxToRemove) { + if ($KeepPackages -contains $pkg) { + Write-Log " KEEP $pkg" -Level INFO + continue + } + + # Installed packages (current user + all users) + $installed = Get-AppxPackage -Name $pkg -AllUsers -ErrorAction SilentlyContinue + if ($installed) { + try { + $installed | Remove-AppxPackage -AllUsers -ErrorAction Stop + Write-Log " Removed AppxPackage: $pkg" -Level OK + } + catch { + Write-Log " Failed to remove AppxPackage $pkg - $_" -Level WARN + } + } + + # Provisioned packages (for new users) + $provisioned = Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -eq $pkg } + if ($provisioned) { + try { + $provisioned | Remove-AppxProvisionedPackage -Online -ErrorAction Stop | Out-Null + Write-Log " Removed provisioned: $pkg" -Level OK + } + catch { + Write-Log " Failed to remove provisioned $pkg - $_" -Level WARN + } + } + + if (-not $installed -and -not $provisioned) { + Write-Log " Not found (already removed): $pkg" -Level INFO + } +} + +# ----------------------------------------------------------------------- +# 1b - Windows Capabilities +# ----------------------------------------------------------------------- +$CapabilitiesToRemove = @( + "Print.Fax.Scan" + "Language.Handwriting" + "Browser.InternetExplorer" + "MathRecognizer" + "OneCoreUAP.OneSync" + "OpenSSH.Client" + "Microsoft.Windows.MSPaint" + "Microsoft.Windows.PowerShell.ISE" + "App.Support.QuickAssist" + "Microsoft.Windows.SnippingTool" + "App.StepsRecorder" + "Hello.Face" + "Media.WindowsMediaPlayer" + "Microsoft.Windows.WordPad" +) + +Write-Log "1b - Removing Windows Capabilities" -Level STEP + +$installedCaps = Get-WindowsCapability -Online -ErrorAction SilentlyContinue + +foreach ($cap in $CapabilitiesToRemove) { + # Match by prefix (e.g. Hello.Face matches Hello.Face.20134.0.0.0) + $matches = $installedCaps | Where-Object { + $_.Name -like "$cap*" -and $_.State -eq "Installed" + } + if ($matches) { + foreach ($c in $matches) { + try { + Remove-WindowsCapability -Online -Name $c.Name -ErrorAction Stop | Out-Null + Write-Log " Removed capability: $($c.Name)" -Level OK + } + catch { + Write-Log " Failed to remove capability $($c.Name) - $_" -Level WARN + } + } + } else { + Write-Log " Not found or not installed: $cap" -Level INFO + } +} + +# ----------------------------------------------------------------------- +# 1c - Windows Optional Features +# ----------------------------------------------------------------------- +$FeaturesToDisable = @( + "MediaPlayback" + "MicrosoftWindowsPowerShellV2Root" + "Microsoft-RemoteDesktopConnection" + "Recall" + "Microsoft-SnippingTool" +) + +Write-Log "1c - Disabling Windows Optional Features" -Level STEP + +foreach ($feat in $FeaturesToDisable) { + $feature = Get-WindowsOptionalFeature -Online -FeatureName $feat -ErrorAction SilentlyContinue + if ($feature -and $feature.State -eq "Enabled") { + try { + Disable-WindowsOptionalFeature -Online -FeatureName $feat -NoRestart -ErrorAction Stop | Out-Null + Write-Log " Disabled feature: $feat" -Level OK + } + catch { + Write-Log " Failed to disable feature $feat - $_" -Level WARN + } + } else { + Write-Log " Not enabled or not found: $feat" -Level INFO + } +} + +Write-Log "Step 1 complete" -Level OK diff --git a/scripts/02-software.ps1 b/scripts/02-software.ps1 index bc403e2..9e6eded 100644 --- a/scripts/02-software.ps1 +++ b/scripts/02-software.ps1 @@ -1 +1,122 @@ -# TODO: 02-software.ps1 +param( + [object]$Config, + [string]$LogFile +) + +$ErrorActionPreference = "Continue" + +function Write-Log { + param([string]$Message, [string]$Level = "INFO") + $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" + Add-Content -Path $LogFile -Value $line -Encoding UTF8 +} + +# ----------------------------------------------------------------------- +# Check winget availability +# ----------------------------------------------------------------------- +Write-Log "Checking winget availability" -Level INFO + +$winget = Get-Command winget -ErrorAction SilentlyContinue +if (-not $winget) { + # Try to find winget in known locations + $wingetPaths = @( + "$env:LOCALAPPDATA\Microsoft\WindowsApps\winget.exe" + "$env:ProgramFiles\WindowsApps\Microsoft.DesktopAppInstaller*\winget.exe" + ) + foreach ($p in $wingetPaths) { + $found = Get-Item $p -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($found) { $winget = $found.FullName; break } + } +} + +if (-not $winget) { + Write-Log "winget not found - software installation skipped" -Level ERROR + exit 1 +} + +Write-Log "winget found: $($winget.Source -or $winget)" -Level OK + +# Accept agreements upfront +& winget source update --accept-source-agreements 2>&1 | Out-Null + +# ----------------------------------------------------------------------- +# Install packages from config +# ----------------------------------------------------------------------- +if (-not $Config -or -not $Config.software -or -not $Config.software.install) { + Write-Log "No software list in config - skipping installs" -Level WARN +} else { + foreach ($pkg in $Config.software.install) { + Write-Log "Installing $($pkg.name) ($($pkg.wingetId))" -Level INFO + $result = & winget install --id $pkg.wingetId ` + --silent ` + --accept-package-agreements ` + --accept-source-agreements ` + --disable-interactivity ` + 2>&1 + + $exitCode = $LASTEXITCODE + if ($exitCode -eq 0) { + Write-Log " Installed OK: $($pkg.name)" -Level OK + } elseif ($exitCode -eq -1978335189) { + # 0x8A150011 = already installed + Write-Log " Already installed: $($pkg.name)" -Level OK + } else { + Write-Log " Failed: $($pkg.name) (exit $exitCode)" -Level ERROR + Write-Log " Output: $($result -join ' ')" -Level ERROR + } + } +} + +# ----------------------------------------------------------------------- +# Set Adobe Reader as default PDF app +# ----------------------------------------------------------------------- +$forcePdf = $true +if ($Config -and $Config.pdfDefault) { + $forcePdf = [bool]$Config.pdfDefault.forceAdobeReader +} + +if ($forcePdf) { + Write-Log "Setting Adobe Reader as default PDF app" -Level INFO + + # Find AcroRd32.exe + $acroPaths = @( + "${env:ProgramFiles(x86)}\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe" + "$env:ProgramFiles\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe" + "${env:ProgramFiles(x86)}\Adobe\Reader\Reader\AcroRd32.exe" + ) + $acroExe = $acroPaths | Where-Object { Test-Path $_ } | Select-Object -First 1 + + if (-not $acroExe) { + Write-Log " AcroRd32.exe not found - PDF default not set" -Level WARN + } else { + Write-Log " Found: $acroExe" -Level INFO + + # Set file type association via HKCR (system-wide, requires admin) + $progId = "AcroExch.Document.DC" + $openCmd = "`"$acroExe`" `"%1`"" + + # HKCR\.pdf -> progId + if (-not (Test-Path "HKCR:\.pdf")) { + New-Item -Path "HKCR:\.pdf" -Force | Out-Null + } + Set-ItemProperty -Path "HKCR:\.pdf" -Name "(Default)" -Value $progId + + # HKCR\AcroExch.Document.DC\shell\open\command + $cmdPath = "HKCR:\$progId\shell\open\command" + if (-not (Test-Path $cmdPath)) { + New-Item -Path $cmdPath -Force | Out-Null + } + Set-ItemProperty -Path $cmdPath -Name "(Default)" -Value $openCmd + + # Also set in HKCU for current user (UserChoice) + $ucPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.pdf\UserChoice" + if (-not (Test-Path $ucPath)) { + New-Item -Path $ucPath -Force | Out-Null + } + Set-ItemProperty -Path $ucPath -Name "ProgId" -Value $progId + + Write-Log " PDF default set to AcroRd32" -Level OK + } +} + +Write-Log "Step 2 complete" -Level OK diff --git a/scripts/03-system-registry.ps1 b/scripts/03-system-registry.ps1 index f064829..d4a759d 100644 --- a/scripts/03-system-registry.ps1 +++ b/scripts/03-system-registry.ps1 @@ -1 +1,179 @@ -# TODO: 03-system-registry.ps1 +param( + [object]$Config, + [string]$LogFile +) + +$ErrorActionPreference = "Continue" + +function Write-Log { + param([string]$Message, [string]$Level = "INFO") + $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" + Add-Content -Path $LogFile -Value $line -Encoding UTF8 +} + +function Set-Reg { + param( + [string]$Path, + [string]$Name, + $Value, + [string]$Type = "DWord" + ) + try { + if (-not (Test-Path $Path)) { + New-Item -Path $Path -Force | Out-Null + } + Set-ItemProperty -Path $Path -Name $Name -Value $Value -Type $Type -Force + Write-Log " SET $Path\$Name = $Value" -Level OK + } + catch { + Write-Log " FAILED $Path\$Name - $_" -Level ERROR + } +} + +function Remove-Reg { + param([string]$Path, [string]$Name) + try { + if (Test-Path $Path) { + Remove-ItemProperty -Path $Path -Name $Name -Force -ErrorAction SilentlyContinue + Write-Log " REMOVED $Path\$Name" -Level OK + } + } + catch { + Write-Log " FAILED removing $Path\$Name - $_" -Level ERROR + } +} + +Write-Log "3 - Applying HKLM system registry tweaks" -Level STEP + +# ----------------------------------------------------------------------- +# Bypass Network Requirement on OOBE (BypassNRO) +# ----------------------------------------------------------------------- +Set-Reg -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" ` + -Name "BypassNRO" -Value 1 + +# ----------------------------------------------------------------------- +# Disable auto-install of Teams (Chat) +# ----------------------------------------------------------------------- +Set-Reg -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications" ` + -Name "ConfigureChatAutoInstall" -Value 0 + +# ----------------------------------------------------------------------- +# Disable Cloud Optimized Content (ads in Start menu etc.) +# ----------------------------------------------------------------------- +Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent" ` + -Name "DisableCloudOptimizedContent" -Value 1 + +Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent" ` + -Name "DisableWindowsConsumerFeatures" -Value 1 + +# ----------------------------------------------------------------------- +# Disable Widgets (News and Interests) +# ----------------------------------------------------------------------- +Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Dsh" ` + -Name "AllowNewsAndInterests" -Value 0 + +# ----------------------------------------------------------------------- +# Microsoft Edge - hide First Run Experience +# ----------------------------------------------------------------------- +Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Edge" ` + -Name "HideFirstRunExperience" -Value 1 + +# Also disable Edge desktop shortcut creation after install +Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\EdgeUpdate" ` + -Name "CreateDesktopShortcutDefault" -Value 0 + +# ----------------------------------------------------------------------- +# Password - no expiration +# ----------------------------------------------------------------------- +Write-Log " Setting password max age to UNLIMITED" -Level INFO +$pwResult = & net accounts /maxpwage:UNLIMITED 2>&1 +if ($LASTEXITCODE -eq 0) { + Write-Log " Password max age set to UNLIMITED" -Level OK +} else { + Write-Log " Failed to set password max age: $pwResult" -Level ERROR +} + +# ----------------------------------------------------------------------- +# Time zone +# ----------------------------------------------------------------------- +$tz = "Central Europe Standard Time" +if ($Config -and $Config.deployment -and $Config.deployment.timezone) { + $tz = $Config.deployment.timezone +} +Write-Log " Setting time zone: $tz" -Level INFO +try { + Set-TimeZone -Id $tz -ErrorAction Stop + Write-Log " Time zone set: $tz" -Level OK +} +catch { + Write-Log " Failed to set time zone: $_" -Level ERROR +} + +# ----------------------------------------------------------------------- +# OneDrive - prevent setup and remove shortcuts +# ----------------------------------------------------------------------- +Write-Log " Disabling OneDrive" -Level INFO + +# Disable OneDrive via policy +Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\OneDrive" ` + -Name "DisableFileSyncNGSC" -Value 1 + +# Remove OneDriveSetup.exe if present +$oneDrivePaths = @( + "$env:SystemRoot\System32\OneDriveSetup.exe" + "$env:SystemRoot\SysWOW64\OneDriveSetup.exe" +) +foreach ($odPath in $oneDrivePaths) { + if (Test-Path $odPath) { + try { + # Uninstall first + & $odPath /uninstall 2>&1 | Out-Null + Write-Log " OneDrive uninstalled via $odPath" -Level OK + } + catch { + Write-Log " OneDrive uninstall failed: $_" -Level WARN + } + } +} + +# Remove OneDrive Start Menu shortcut +$odLnk = "$env:ProgramData\Microsoft\Windows\Start Menu\Programs\OneDrive.lnk" +if (Test-Path $odLnk) { + Remove-Item $odLnk -Force -ErrorAction SilentlyContinue + Write-Log " Removed OneDrive Start Menu shortcut" -Level OK +} + +# ----------------------------------------------------------------------- +# Outlook (new) - disable auto-install via UScheduler +# ----------------------------------------------------------------------- +Write-Log " Disabling Outlook (new) auto-install" -Level INFO + +$uschedulerPaths = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate" + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Orchestrator\UScheduler\OutlookUpdate" +) +foreach ($uPath in $uschedulerPaths) { + if (Test-Path $uPath) { + try { + Remove-Item -Path $uPath -Recurse -Force + Write-Log " Removed UScheduler key: $uPath" -Level OK + } + catch { + Write-Log " Failed to remove UScheduler key: $_" -Level WARN + } + } +} + +# ----------------------------------------------------------------------- +# Disable GameDVR +# ----------------------------------------------------------------------- +Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\GameDVR" ` + -Name "AllowGameDVR" -Value 0 + +# ----------------------------------------------------------------------- +# Disable Recall (Windows AI feature) +# ----------------------------------------------------------------------- +Set-Reg -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsAI" ` + -Name "DisableAIDataAnalysis" -Value 1 + +Write-Log "Step 3 complete" -Level OK diff --git a/scripts/04-default-profile.ps1 b/scripts/04-default-profile.ps1 index 276484b..225d22b 100644 --- a/scripts/04-default-profile.ps1 +++ b/scripts/04-default-profile.ps1 @@ -1 +1,244 @@ -# TODO: 04-default-profile.ps1 +param( + [object]$Config, + [string]$LogFile +) + +$ErrorActionPreference = "Continue" + +function Write-Log { + param([string]$Message, [string]$Level = "INFO") + $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" + Add-Content -Path $LogFile -Value $line -Encoding UTF8 +} + +# ----------------------------------------------------------------------- +# Helper - apply a registry setting to both Default hive and current HKCU +# ----------------------------------------------------------------------- +function Set-ProfileReg { + param( + [string]$SubKey, # relative to HKCU (e.g. "Software\Microsoft\...") + [string]$Name, + $Value, + [string]$Type = "DWord" + ) + + # Apply to loaded Default hive + $defPath = "Registry::HKU\DefaultProfile\$SubKey" + try { + if (-not (Test-Path $defPath)) { + New-Item -Path $defPath -Force | Out-Null + } + Set-ItemProperty -Path $defPath -Name $Name -Value $Value -Type $Type -Force + } + catch { + Write-Log " DEFAULT HIVE failed $SubKey\$Name - $_" -Level ERROR + } + + # Apply to current user as well + $hkcuPath = "HKCU:\$SubKey" + try { + if (-not (Test-Path $hkcuPath)) { + New-Item -Path $hkcuPath -Force | Out-Null + } + Set-ItemProperty -Path $hkcuPath -Name $Name -Value $Value -Type $Type -Force + Write-Log " SET $SubKey\$Name = $Value" -Level OK + } + catch { + Write-Log " HKCU failed $SubKey\$Name - $_" -Level ERROR + } +} + +function Remove-ProfileReg { + param([string]$SubKey, [string]$Name) + + $defPath = "Registry::HKU\DefaultProfile\$SubKey" + try { + if (Test-Path $defPath) { + Remove-ItemProperty -Path $defPath -Name $Name -Force -ErrorAction SilentlyContinue + } + } + catch { } + + $hkcuPath = "HKCU:\$SubKey" + try { + if (Test-Path $hkcuPath) { + Remove-ItemProperty -Path $hkcuPath -Name $Name -Force -ErrorAction SilentlyContinue + Write-Log " REMOVED $SubKey\$Name" -Level OK + } + } + catch { + Write-Log " FAILED removing $SubKey\$Name - $_" -Level ERROR + } +} + +# ----------------------------------------------------------------------- +# Load Default profile hive +# ----------------------------------------------------------------------- +$hivePath = "C:\Users\Default\NTUSER.DAT" +$hiveKey = "DefaultProfile" + +Write-Log "Loading Default hive: $hivePath" -Level INFO + +# Unload first in case previous run left it mounted +& reg unload "HKU\$hiveKey" 2>&1 | Out-Null + +$loadResult = & reg load "HKU\$hiveKey" $hivePath 2>&1 +if ($LASTEXITCODE -ne 0) { + Write-Log "Failed to load Default hive: $loadResult" -Level ERROR + exit 1 +} +Write-Log "Default hive loaded" -Level OK + +try { + # ----------------------------------------------------------------------- + # Taskbar settings (Win10 + Win11) + # ----------------------------------------------------------------------- + Write-Log "Applying taskbar settings" -Level STEP + + $tbPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" + + # Win11: align taskbar to left (0 = left, 1 = center) + Set-ProfileReg -SubKey $tbPath -Name "TaskbarAl" -Value 0 + + # Hide Search box / button (0 = hidden, 1 = icon, 2 = full box) + Set-ProfileReg -SubKey $tbPath -Name "SearchboxTaskbarMode" -Value 0 + + # Hide Task View button + Set-ProfileReg -SubKey $tbPath -Name "ShowTaskViewButton" -Value 0 + + # Hide Widgets button + Set-ProfileReg -SubKey $tbPath -Name "TaskbarDa" -Value 0 + + # Hide Chat / Teams button + Set-ProfileReg -SubKey $tbPath -Name "TaskbarMn" -Value 0 + + # Hide Copilot button + Set-ProfileReg -SubKey $tbPath -Name "ShowCopilotButton" -Value 0 + + # Show file extensions in Explorer + Set-ProfileReg -SubKey $tbPath -Name "HideFileExt" -Value 0 + + # Open Explorer to This PC instead of Quick Access + Set-ProfileReg -SubKey $tbPath -Name "LaunchTo" -Value 1 + + # ----------------------------------------------------------------------- + # System tray - show all icons + # ----------------------------------------------------------------------- + Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Explorer" ` + -Name "EnableAutoTray" -Value 0 + + # ----------------------------------------------------------------------- + # Start menu settings + # ----------------------------------------------------------------------- + Write-Log "Applying Start menu settings" -Level STEP + + # Disable Bing search suggestions in Start menu + Set-ProfileReg -SubKey "Software\Policies\Microsoft\Windows\Explorer" ` + -Name "DisableSearchBoxSuggestions" -Value 1 + + # Win11: empty Start menu pins + $startPinsPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" + Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Start" ` + -Name "ConfigureStartPins" ` + -Value '{"pinnedList":[]}' ` + -Type "String" + + # ----------------------------------------------------------------------- + # Copilot - disable + # ----------------------------------------------------------------------- + Set-ProfileReg -SubKey "Software\Policies\Microsoft\Windows\WindowsCopilot" ` + -Name "TurnOffWindowsCopilot" -Value 1 + + # ----------------------------------------------------------------------- + # GameDVR - disable + # ----------------------------------------------------------------------- + Set-ProfileReg -SubKey "System\GameConfigStore" ` + -Name "GameDVR_Enabled" -Value 0 + + Set-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\GameDVR" ` + -Name "AppCaptureEnabled" -Value 0 + + # ----------------------------------------------------------------------- + # Num Lock on startup + # ----------------------------------------------------------------------- + Set-ProfileReg -SubKey "Control Panel\Keyboard" ` + -Name "InitialKeyboardIndicators" -Value 2 -Type "String" + + # ----------------------------------------------------------------------- + # Accent color on title bars + # ----------------------------------------------------------------------- + Set-ProfileReg -SubKey "Software\Microsoft\Windows\DWM" ` + -Name "ColorPrevalence" -Value 1 + + # ----------------------------------------------------------------------- + # OneDrive - remove RunOnce key from Default profile + # ----------------------------------------------------------------------- + Write-Log "Removing OneDrive from Default profile RunOnce" -Level INFO + Remove-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\Run" ` + -Name "OneDriveSetup" + + Remove-ProfileReg -SubKey "Software\Microsoft\Windows\CurrentVersion\RunOnce" ` + -Name "Delete Cached Standalone Update Binary" + + # Remove OneDrive from Explorer namespace (left panel) + $oneDriveClsid = "{018D5C66-4533-4307-9B53-224DE2ED1FE6}" + $nsPath = "Software\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\$oneDriveClsid" + $defNsPath = "Registry::HKU\DefaultProfile\$nsPath" + if (Test-Path $defNsPath) { + Remove-Item -Path $defNsPath -Recurse -Force -ErrorAction SilentlyContinue + Write-Log " Removed OneDrive from Explorer namespace (Default)" -Level OK + } + $hkcuNsPath = "HKCU:\$nsPath" + if (Test-Path $hkcuNsPath) { + Remove-Item -Path $hkcuNsPath -Recurse -Force -ErrorAction SilentlyContinue + Write-Log " Removed OneDrive from Explorer namespace (HKCU)" -Level OK + } + + # ----------------------------------------------------------------------- + # Empty taskbar pinned apps (Win10/11) + # ----------------------------------------------------------------------- + Write-Log "Clearing taskbar pinned apps layout" -Level INFO + + $taskbarLayoutDir = "C:\Users\Default\AppData\Local\Microsoft\Windows\Shell" + if (-not (Test-Path $taskbarLayoutDir)) { + New-Item -ItemType Directory -Path $taskbarLayoutDir -Force | Out-Null + } + + $taskbarLayoutXml = @" + + + + + + + + + +"@ + $taskbarLayoutXml | Set-Content -Path "$taskbarLayoutDir\LayoutModification.xml" -Encoding UTF8 -Force + Write-Log " Taskbar LayoutModification.xml written" -Level OK + +} +finally { + # ----------------------------------------------------------------------- + # Unload Default hive - always, even on error + # ----------------------------------------------------------------------- + Write-Log "Unloading Default hive" -Level INFO + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + Start-Sleep -Milliseconds 500 + + $unloadResult = & reg unload "HKU\$hiveKey" 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Log "Default hive unloaded" -Level OK + } else { + Write-Log "Failed to unload Default hive: $unloadResult" -Level ERROR + } +} + +Write-Log "Step 4 complete" -Level OK diff --git a/scripts/05-personalization.ps1 b/scripts/05-personalization.ps1 index 649d008..0e78771 100644 --- a/scripts/05-personalization.ps1 +++ b/scripts/05-personalization.ps1 @@ -1 +1,166 @@ -# TODO: 05-personalization.ps1 +param( + [object]$Config, + [string]$LogFile +) + +$ErrorActionPreference = "Continue" + +function Write-Log { + param([string]$Message, [string]$Level = "INFO") + $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" + Add-Content -Path $LogFile -Value $line -Encoding UTF8 +} + +# Accent color #223B47 stored as ABGR DWORD: 0xFF473B22 +# A=FF B=47 G=3B R=22 -> 0xFF473B22 = 4283612962 +$AccentColorABGR = 0xFF473B22 + +# Gradient colors (Windows generates these automatically but we set them explicitly) +# AccentPalette is 32 bytes - 8 shades of the accent color (BGRA each) +# We use the same color for all shades as a safe default +$AccentColorHex = "#223B47" + +function Set-Reg { + param([string]$Path, [string]$Name, $Value, [string]$Type = "DWord") + try { + if (-not (Test-Path $Path)) { New-Item -Path $Path -Force | Out-Null } + Set-ItemProperty -Path $Path -Name $Name -Value $Value -Type $Type -Force + Write-Log " SET $Path\$Name = $Value" -Level OK + } + catch { + Write-Log " FAILED $Path\$Name - $_" -Level ERROR + } +} + +function Apply-ThemeSettings { + param([string]$HiveRoot) # "HKCU:" or "Registry::HKU\DefaultProfile" + + # ----------------------------------------------------------------------- + # System theme - Dark (taskbar, Start, action center) + # ----------------------------------------------------------------------- + Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" ` + -Name "SystemUsesLightTheme" -Value 0 + + # ----------------------------------------------------------------------- + # App theme - Light + # ----------------------------------------------------------------------- + Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" ` + -Name "AppsUseLightTheme" -Value 1 + + # ----------------------------------------------------------------------- + # Accent color on Start and taskbar + # ----------------------------------------------------------------------- + Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" ` + -Name "ColorPrevalence" -Value 1 + + # ----------------------------------------------------------------------- + # Transparency effects - disabled + # ----------------------------------------------------------------------- + Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" ` + -Name "EnableTransparency" -Value 0 + + # ----------------------------------------------------------------------- + # Accent color + # ----------------------------------------------------------------------- + Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\DWM" ` + -Name "AccentColor" -Value $AccentColorABGR -Type "DWord" + + Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\DWM" ` + -Name "ColorizationColor" -Value $AccentColorABGR -Type "DWord" + + Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\DWM" ` + -Name "ColorizationAfterglow" -Value $AccentColorABGR -Type "DWord" + + # Accent color on title bars and borders + Set-Reg -Path "$HiveRoot\Software\Microsoft\Windows\DWM" ` + -Name "ColorPrevalence" -Value 1 + + # ----------------------------------------------------------------------- + # Wallpaper - solid color #223B47 (fallback before DesktopInfo runs) + # ----------------------------------------------------------------------- + # Background color as decimal RGB + Set-Reg -Path "$HiveRoot\Control Panel\Colors" ` + -Name "Background" -Value "34 59 71" -Type "String" + + Set-Reg -Path "$HiveRoot\Control Panel\Desktop" ` + -Name "WallpaperStyle" -Value "0" -Type "String" + + Set-Reg -Path "$HiveRoot\Control Panel\Desktop" ` + -Name "TileWallpaper" -Value "0" -Type "String" +} + +# ----------------------------------------------------------------------- +# Load Default hive +# ----------------------------------------------------------------------- +$hivePath = "C:\Users\Default\NTUSER.DAT" +$hiveKey = "DefaultProfile" + +Write-Log "Loading Default hive for personalization" -Level INFO + +& reg unload "HKU\$hiveKey" 2>&1 | Out-Null +$loadResult = & reg load "HKU\$hiveKey" $hivePath 2>&1 +if ($LASTEXITCODE -ne 0) { + Write-Log "Failed to load Default hive: $loadResult" -Level ERROR + Write-Log "Applying personalization to current user only" -Level WARN + + Write-Log "Applying theme to current user (HKCU)" -Level STEP + Apply-ThemeSettings -HiveRoot "HKCU:" + + # Set wallpaper via SystemParametersInfo for current user + Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; +public class WallpaperHelper { + [DllImport("user32.dll", CharSet=CharSet.Auto)] + public static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni); +} +"@ -ErrorAction SilentlyContinue + [WallpaperHelper]::SystemParametersInfo(20, 0, "", 3) | Out-Null + exit 0 +} + +try { + Write-Log "Applying theme to Default hive" -Level STEP + Apply-ThemeSettings -HiveRoot "Registry::HKU\DefaultProfile" + + Write-Log "Applying theme to current user (HKCU)" -Level STEP + Apply-ThemeSettings -HiveRoot "HKCU:" +} +finally { + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + Start-Sleep -Milliseconds 500 + + $unloadResult = & reg unload "HKU\$hiveKey" 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Log "Default hive unloaded" -Level OK + } else { + Write-Log "Failed to unload Default hive: $unloadResult" -Level ERROR + } +} + +# ----------------------------------------------------------------------- +# Apply wallpaper (solid color) to current desktop session +# ----------------------------------------------------------------------- +Write-Log "Setting desktop wallpaper to solid color" -Level INFO + +try { + Add-Type -TypeDefinition @" +using System; +using System.Runtime.InteropServices; +public class WallpaperHelper { + [DllImport("user32.dll", CharSet=CharSet.Auto)] + public static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni); +} +"@ -ErrorAction SilentlyContinue + + # SPI_SETDESKTOPWALLPAPER=20, SPIF_UPDATEINIFILE|SPIF_SENDCHANGE=3 + # Empty string = solid color defined in Control Panel\Colors\Background + [WallpaperHelper]::SystemParametersInfo(20, 0, "", 3) | Out-Null + Write-Log " Desktop wallpaper updated" -Level OK +} +catch { + Write-Log " Failed to update wallpaper: $_" -Level WARN +} + +Write-Log "Step 5 complete" -Level OK diff --git a/scripts/06-scheduled-tasks.ps1 b/scripts/06-scheduled-tasks.ps1 index 85df77a..31b6745 100644 --- a/scripts/06-scheduled-tasks.ps1 +++ b/scripts/06-scheduled-tasks.ps1 @@ -1 +1,161 @@ -# TODO: 06-scheduled-tasks.ps1 +param( + [object]$Config, + [string]$LogFile +) + +$ErrorActionPreference = "Continue" + +function Write-Log { + param([string]$Message, [string]$Level = "INFO") + $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" + Add-Content -Path $LogFile -Value $line -Encoding UTF8 +} + +$ScriptDir = "C:\Windows\Setup\Scripts" +if (-not (Test-Path $ScriptDir)) { + New-Item -ItemType Directory -Path $ScriptDir -Force | Out-Null +} + +function Register-Task { + param( + [string]$TaskName, + [string]$Description, + [object]$Action, + [object]$Trigger, + [string]$RunLevel = "Highest" + ) + try { + # Remove existing task with same name + Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue + + $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 5) ` + -MultipleInstances IgnoreNew ` + -StartWhenAvailable + + $principal = New-ScheduledTaskPrincipal -GroupId "Users" ` + -RunLevel $RunLevel + + $task = New-ScheduledTask -Action $Action ` + -Trigger $Trigger ` + -Settings $settings ` + -Principal $principal ` + -Description $Description + + Register-ScheduledTask -TaskName $TaskName -InputObject $task -Force | Out-Null + Write-Log " Registered task: $TaskName" -Level OK + } + catch { + Write-Log " Failed to register task $TaskName - $_" -Level ERROR + } +} + +# ----------------------------------------------------------------------- +# Task: ShowAllTrayIcons +# Runs on logon + every 1 minute, sets EnableAutoTray=0 so all tray icons +# are always visible (Win11 hides them by default) +# ----------------------------------------------------------------------- +Write-Log "Registering task: ShowAllTrayIcons" -Level STEP + +$showTrayScript = "$ScriptDir\ShowAllTrayIcons.ps1" +@' +$regPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer" +Set-ItemProperty -Path $regPath -Name "EnableAutoTray" -Value 0 -Force +Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue +'@ | Set-Content -Path $showTrayScript -Encoding UTF8 -Force + +$showTrayAction = New-ScheduledTaskAction -Execute "powershell.exe" ` + -Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$showTrayScript`"" +$showTrayTrigger = @( + $(New-ScheduledTaskTrigger -AtLogOn), + $(New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes 1) -Once -At (Get-Date)) +) + +Register-Task -TaskName "ShowAllTrayIcons" ` + -Description "Show all system tray icons for current user" ` + -Action $showTrayAction ` + -Trigger $showTrayTrigger[0] + +# ----------------------------------------------------------------------- +# Task: PDF-DefaultApp +# Runs on every logon, restores .pdf -> Adobe Reader association +# Guards against Edge overwriting it +# ----------------------------------------------------------------------- +Write-Log "Registering task: PDF-DefaultApp" -Level STEP + +$pdfScript = "$ScriptDir\PDF-DefaultApp.ps1" +@' +# Restore .pdf -> Adobe Reader association +$acroPaths = @( + "${env:ProgramFiles(x86)}\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe" + "$env:ProgramFiles\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe" + "${env:ProgramFiles(x86)}\Adobe\Reader\Reader\AcroRd32.exe" +) +$acroExe = $acroPaths | Where-Object { Test-Path $_ } | Select-Object -First 1 +if (-not $acroExe) { exit 0 } + +$progId = "AcroExch.Document.DC" + +# Check current association +$current = (Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.pdf\UserChoice" ` + -Name "ProgId" -ErrorAction SilentlyContinue).ProgId + +if ($current -ne $progId) { + $ucPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.pdf\UserChoice" + if (-not (Test-Path $ucPath)) { New-Item -Path $ucPath -Force | Out-Null } + Set-ItemProperty -Path $ucPath -Name "ProgId" -Value $progId -Force +} +'@ | Set-Content -Path $pdfScript -Encoding UTF8 -Force + +$pdfAction = New-ScheduledTaskAction -Execute "powershell.exe" ` + -Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$pdfScript`"" +$pdfTrigger = New-ScheduledTaskTrigger -AtLogOn + +Register-Task -TaskName "PDF-DefaultApp" ` + -Description "Restore Adobe Reader as default PDF app on logon" ` + -Action $pdfAction ` + -Trigger $pdfTrigger + +# ----------------------------------------------------------------------- +# Task: UnlockStartLayout +# Runs once after deployment to unlock the Start menu layout +# so users can still customize it later +# ----------------------------------------------------------------------- +Write-Log "Registering task: UnlockStartLayout" -Level STEP + +$unlockScript = "$ScriptDir\UnlockStartLayout.ps1" +@' +# Remove Start layout lock so users can modify it +$layoutXml = "C:\Users\Default\AppData\Local\Microsoft\Windows\Shell\LayoutModification.xml" +if (Test-Path $layoutXml) { + Remove-Item $layoutXml -Force -ErrorAction SilentlyContinue +} + +# Unregister self after running once +Unregister-ScheduledTask -TaskName "UnlockStartLayout" -Confirm:$false -ErrorAction SilentlyContinue +'@ | Set-Content -Path $unlockScript -Encoding UTF8 -Force + +$unlockAction = New-ScheduledTaskAction -Execute "powershell.exe" ` + -Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$unlockScript`"" +# Trigger: 5 minutes after system startup, once +$unlockTrigger = New-ScheduledTaskTrigger -AtStartup +$unlockTrigger.Delay = "PT5M" + +$unlockPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest +$unlockSettings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 10) ` + -StartWhenAvailable +$unlockTask = New-ScheduledTask -Action $unlockAction ` + -Trigger $unlockTrigger ` + -Settings $unlockSettings ` + -Principal $unlockPrincipal ` + -Description "Unlock Start menu layout 5 min after first boot" + +try { + Unregister-ScheduledTask -TaskName "UnlockStartLayout" -Confirm:$false -ErrorAction SilentlyContinue + Register-ScheduledTask -TaskName "UnlockStartLayout" -InputObject $unlockTask -Force | Out-Null + Write-Log " Registered task: UnlockStartLayout" -Level OK +} +catch { + Write-Log " Failed to register task UnlockStartLayout - $_" -Level ERROR +} + +Write-Log "Step 6 complete" -Level OK diff --git a/scripts/07-desktop-info.ps1 b/scripts/07-desktop-info.ps1 index 426bdfd..7e836b8 100644 --- a/scripts/07-desktop-info.ps1 +++ b/scripts/07-desktop-info.ps1 @@ -1 +1,217 @@ -# TODO: 07-desktop-info.ps1 +param( + [object]$Config, + [string]$LogFile +) + +$ErrorActionPreference = "Continue" + +function Write-Log { + param([string]$Message, [string]$Level = "INFO") + $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" + Add-Content -Path $LogFile -Value $line -Encoding UTF8 +} + +$ScriptDir = "C:\Windows\Setup\Scripts" +$RenderScript = "$ScriptDir\DesktopInfo-Render.ps1" +$BmpPath = "$ScriptDir\desktopinfo.bmp" + +if (-not (Test-Path $ScriptDir)) { + New-Item -ItemType Directory -Path $ScriptDir -Force | Out-Null +} + +# ----------------------------------------------------------------------- +# Read display settings from config +# ----------------------------------------------------------------------- +$fontSize = 13 +$fontColor = "#FFFFFF" +$position = "bottomRight" + +if ($Config -and $Config.desktopInfo) { + if ($Config.desktopInfo.fontSize) { $fontSize = [int]$Config.desktopInfo.fontSize } + if ($Config.desktopInfo.fontColor) { $fontColor = $Config.desktopInfo.fontColor } + if ($Config.desktopInfo.position) { $position = $Config.desktopInfo.position } +} + +# ----------------------------------------------------------------------- +# Write the rendering script (runs on every logon as the user) +# ----------------------------------------------------------------------- +Write-Log "Writing DesktopInfo render script to $RenderScript" -Level INFO + +$renderContent = @" +# DesktopInfo-Render.ps1 +# Collects system info and renders it onto the desktop wallpaper. +# Runs on every user logon via Scheduled Task. + +`$ErrorActionPreference = "Continue" + +Add-Type -AssemblyName System.Drawing +Add-Type -TypeDefinition @' +using System; +using System.Runtime.InteropServices; +public class WallpaperApi { + [DllImport("user32.dll", CharSet=CharSet.Auto)] + public static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni); +} +'@ -ErrorAction SilentlyContinue + +# ----------------------------------------------------------------------- +# Collect system info +# ----------------------------------------------------------------------- +`$hostname = `$env:COMPUTERNAME +`$username = `$env:USERNAME +`$ipAddress = (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | + Where-Object { `$_.IPAddress -ne "127.0.0.1" -and `$_.PrefixOrigin -ne "WellKnown" } | + Select-Object -First 1).IPAddress +if (-not `$ipAddress) { `$ipAddress = "N/A" } + +`$osInfo = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue +`$osName = if (`$osInfo) { `$osInfo.Caption -replace "Microsoft ", "" } else { "Windows" } +`$osBuild = if (`$osInfo) { `$osInfo.BuildNumber } else { "" } + +# Deployment date = when script was first run, stored in registry +`$deployRegPath = "HKLM:\SOFTWARE\X9\Deployment" +`$deployDate = (Get-ItemProperty -Path `$deployRegPath -Name "DeployDate" -ErrorAction SilentlyContinue).DeployDate +if (-not `$deployDate) { `$deployDate = "N/A" } + +# ----------------------------------------------------------------------- +# Build info lines +# ----------------------------------------------------------------------- +`$lines = @( + "Computer : `$hostname" + "User : `$username" + "IP : `$ipAddress" + "OS : `$osName (build `$osBuild)" + "Deployed : `$deployDate" +) + +# ----------------------------------------------------------------------- +# Render bitmap +# ----------------------------------------------------------------------- +Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue + +`$screen = [System.Windows.Forms.Screen]::PrimaryScreen +`$width = if (`$screen) { `$screen.Bounds.Width } else { 1920 } +`$height = if (`$screen) { `$screen.Bounds.Height } else { 1080 } + +`$bmp = New-Object System.Drawing.Bitmap(`$width, `$height) +`$g = [System.Drawing.Graphics]::FromImage(`$bmp) + +# Background: solid accent color #223B47 +`$bgColor = [System.Drawing.ColorTranslator]::FromHtml("#223B47") +`$g.Clear(`$bgColor) + +# Font and colors +`$fontFamily = "Consolas" +`$fontSize = $fontSize +`$font = New-Object System.Drawing.Font(`$fontFamily, `$fontSize, [System.Drawing.FontStyle]::Regular) +`$brush = New-Object System.Drawing.SolidBrush([System.Drawing.ColorTranslator]::FromHtml("$fontColor")) +`$shadowBrush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(180, 0, 0, 0)) + +# Measure text block +`$lineHeight = `$font.GetHeight(`$g) + 4 +`$blockH = `$lines.Count * `$lineHeight +`$maxWidth = (`$lines | ForEach-Object { `$g.MeasureString(`$_, `$font).Width } | Measure-Object -Maximum).Maximum + +# Position +`$margin = 24 +`$pos = "$position" +`$x = switch -Wildcard (`$pos) { + "*Right" { `$width - `$maxWidth - `$margin } + "*Left" { `$margin } + default { `$margin } +} +`$y = switch -Wildcard (`$pos) { + "bottom*" { `$height - `$blockH - `$margin } + "top*" { `$margin } + default { `$height - `$blockH - `$margin } +} + +# Draw shadow then text +foreach (`$line in `$lines) { + `$g.DrawString(`$line, `$font, `$shadowBrush, (`$x + 1), (`$y + 1)) + `$g.DrawString(`$line, `$font, `$brush, `$x, `$y) + `$y += `$lineHeight +} + +`$g.Dispose() + +# Save BMP +`$bmpPath = "$BmpPath" +`$bmp.Save(`$bmpPath, [System.Drawing.Imaging.ImageFormat]::Bmp) +`$bmp.Dispose() + +# Set as wallpaper +# SPI_SETDESKTOPWALLPAPER=20, SPIF_UPDATEINIFILE|SPIF_SENDCHANGE=3 +[WallpaperApi]::SystemParametersInfo(20, 0, `$bmpPath, 3) | Out-Null +"@ + +$renderContent | Set-Content -Path $RenderScript -Encoding UTF8 -Force +Write-Log "Render script written" -Level OK + +# ----------------------------------------------------------------------- +# Store deployment date in registry (used by render script) +# ----------------------------------------------------------------------- +Write-Log "Storing deployment date in registry" -Level INFO +try { + if (-not (Test-Path "HKLM:\SOFTWARE\X9\Deployment")) { + New-Item -Path "HKLM:\SOFTWARE\X9\Deployment" -Force | Out-Null + } + $existingDate = (Get-ItemProperty -Path "HKLM:\SOFTWARE\X9\Deployment" ` + -Name "DeployDate" -ErrorAction SilentlyContinue).DeployDate + if (-not $existingDate) { + Set-ItemProperty -Path "HKLM:\SOFTWARE\X9\Deployment" ` + -Name "DeployDate" ` + -Value (Get-Date -Format "yyyy-MM-dd") ` + -Force + Write-Log " DeployDate set: $(Get-Date -Format 'yyyy-MM-dd')" -Level OK + } else { + Write-Log " DeployDate already set: $existingDate" -Level INFO + } +} +catch { + Write-Log " Failed to set DeployDate: $_" -Level ERROR +} + +# ----------------------------------------------------------------------- +# Register scheduled task: DesktopInfo +# Runs the render script on every user logon +# ----------------------------------------------------------------------- +Write-Log "Registering task: DesktopInfo" -Level STEP + +try { + Unregister-ScheduledTask -TaskName "DesktopInfo" -Confirm:$false -ErrorAction SilentlyContinue + + $action = New-ScheduledTaskAction -Execute "powershell.exe" ` + -Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$RenderScript`"" + $trigger = New-ScheduledTaskTrigger -AtLogOn + $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2) ` + -MultipleInstances IgnoreNew ` + -StartWhenAvailable + $principal = New-ScheduledTaskPrincipal -GroupId "Users" -RunLevel Limited + + $task = New-ScheduledTask -Action $action ` + -Trigger $trigger ` + -Settings $settings ` + -Principal $principal ` + -Description "Render system info onto desktop wallpaper on logon" + + Register-ScheduledTask -TaskName "DesktopInfo" -InputObject $task -Force | Out-Null + Write-Log "Task DesktopInfo registered" -Level OK +} +catch { + Write-Log "Failed to register DesktopInfo task: $_" -Level ERROR +} + +# ----------------------------------------------------------------------- +# Run once immediately for current user +# ----------------------------------------------------------------------- +Write-Log "Running DesktopInfo render now for current user" -Level INFO +try { + & powershell.exe -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File $RenderScript + Write-Log "DesktopInfo rendered" -Level OK +} +catch { + Write-Log "DesktopInfo render failed: $_" -Level WARN +} + +Write-Log "Step 7 complete" -Level OK diff --git a/tests/Test-Deployment.ps1 b/tests/Test-Deployment.ps1 index c631878..7101c4c 100644 --- a/tests/Test-Deployment.ps1 +++ b/tests/Test-Deployment.ps1 @@ -1 +1,222 @@ -# TODO: Test-Deployment.ps1 +#Requires -RunAsAdministrator + +# Post-deployment verification script. +# Checks that all deployment steps completed correctly. +# Outputs a pass/fail report. + +$ErrorActionPreference = "Continue" + +$PassCount = 0 +$FailCount = 0 +$WarnCount = 0 + +function Test-Check { + param( + [string]$Name, + [scriptblock]$Check, + [switch]$WarnOnly + ) + try { + $result = & $Check + if ($result) { + Write-Host " [PASS] $Name" -ForegroundColor Green + $script:PassCount++ + } else { + if ($WarnOnly) { + Write-Host " [WARN] $Name" -ForegroundColor Yellow + $script:WarnCount++ + } else { + Write-Host " [FAIL] $Name" -ForegroundColor Red + $script:FailCount++ + } + } + } + catch { + Write-Host " [FAIL] $Name (exception: $_)" -ForegroundColor Red + $script:FailCount++ + } +} + +function Get-RegValue { + param([string]$Path, [string]$Name) + try { + return (Get-ItemProperty -Path $Path -Name $Name -ErrorAction Stop).$Name + } + catch { return $null } +} + +Write-Host "" +Write-Host "========================================" +Write-Host " Deployment Verification" +Write-Host " Computer: $env:COMPUTERNAME" +Write-Host " Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm')" +Write-Host "========================================" + +# ----------------------------------------------------------------------- +# Log file +# ----------------------------------------------------------------------- +Write-Host "" +Write-Host "--- Log ---" +Test-Check "Deploy.log exists" { + Test-Path "C:\Windows\Setup\Scripts\Deploy.log" +} + +# ----------------------------------------------------------------------- +# Software +# ----------------------------------------------------------------------- +Write-Host "" +Write-Host "--- Software ---" +Test-Check "7-Zip installed" { + (Get-AppxPackage -Name "7zip.7zip" -ErrorAction SilentlyContinue) -or + (Test-Path "${env:ProgramFiles}\7-Zip\7z.exe") -or + (Test-Path "${env:ProgramFiles(x86)}\7-Zip\7z.exe") +} + +Test-Check "Adobe Acrobat Reader installed" { + (Test-Path "${env:ProgramFiles(x86)}\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe") -or + (Test-Path "$env:ProgramFiles\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe") +} + +Test-Check "OpenVPN Connect installed" { + (Test-Path "$env:ProgramFiles\OpenVPN Connect\OpenVPNConnect.exe") -or + (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" ` + -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like "OpenVPN*" }) +} -WarnOnly + +# ----------------------------------------------------------------------- +# Bloatware +# ----------------------------------------------------------------------- +Write-Host "" +Write-Host "--- Bloatware removal ---" + +$bloatwareToCheck = @( + "Microsoft.549981C3F5F10" # Cortana + "Microsoft.BingNews" + "MicrosoftTeams" + "Microsoft.XboxApp" + "Microsoft.YourPhone" + "Microsoft.ZuneMusic" + "Microsoft.GamingApp" +) + +foreach ($pkg in $bloatwareToCheck) { + Test-Check "Removed: $pkg" { + $installed = Get-AppxPackage -Name $pkg -AllUsers -ErrorAction SilentlyContinue + -not $installed + } -WarnOnly +} + +Test-Check "Calculator kept" { + Get-AppxPackage -Name "Microsoft.WindowsCalculator" -AllUsers -ErrorAction SilentlyContinue +} + +# ----------------------------------------------------------------------- +# System registry (HKLM) +# ----------------------------------------------------------------------- +Write-Host "" +Write-Host "--- System registry ---" + +Test-Check "BypassNRO set" { + (Get-RegValue "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" "BypassNRO") -eq 1 +} + +Test-Check "Teams auto-install disabled" { + (Get-RegValue "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications" "ConfigureChatAutoInstall") -eq 0 +} + +Test-Check "Widgets disabled" { + (Get-RegValue "HKLM:\SOFTWARE\Policies\Microsoft\Dsh" "AllowNewsAndInterests") -eq 0 +} + +Test-Check "Edge First Run hidden" { + (Get-RegValue "HKLM:\SOFTWARE\Policies\Microsoft\Edge" "HideFirstRunExperience") -eq 1 +} + +Test-Check "OneDrive disabled via policy" { + (Get-RegValue "HKLM:\SOFTWARE\Policies\Microsoft\Windows\OneDrive" "DisableFileSyncNGSC") -eq 1 +} + +Test-Check "GameDVR disabled" { + (Get-RegValue "HKLM:\SOFTWARE\Policies\Microsoft\Windows\GameDVR" "AllowGameDVR") -eq 0 +} + +Test-Check "Time zone set" { + (Get-TimeZone).Id -eq "Central Europe Standard Time" +} + +Test-Check "Deployment date in registry" { + (Get-RegValue "HKLM:\SOFTWARE\X9\Deployment" "DeployDate") -ne $null +} + +# ----------------------------------------------------------------------- +# Current user (HKCU) - personalization +# ----------------------------------------------------------------------- +Write-Host "" +Write-Host "--- User settings (current user) ---" + +Test-Check "Dark system theme" { + (Get-RegValue "HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" "SystemUsesLightTheme") -eq 0 +} + +Test-Check "Light app theme" { + (Get-RegValue "HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" "AppsUseLightTheme") -eq 1 +} + +Test-Check "Transparency disabled" { + (Get-RegValue "HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" "EnableTransparency") -eq 0 +} + +Test-Check "Taskbar aligned left" { + (Get-RegValue "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" "TaskbarAl") -eq 0 +} -WarnOnly + +Test-Check "File extensions visible" { + (Get-RegValue "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" "HideFileExt") -eq 0 +} + +Test-Check "Explorer opens to This PC" { + (Get-RegValue "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" "LaunchTo") -eq 1 +} + +# ----------------------------------------------------------------------- +# Scheduled tasks +# ----------------------------------------------------------------------- +Write-Host "" +Write-Host "--- Scheduled tasks ---" + +$tasks = @("ShowAllTrayIcons", "PDF-DefaultApp", "DesktopInfo", "UnlockStartLayout") +foreach ($t in $tasks) { + Test-Check "Task registered: $t" { + Get-ScheduledTask -TaskName $t -ErrorAction SilentlyContinue + } +} + +# ----------------------------------------------------------------------- +# DesktopInfo +# ----------------------------------------------------------------------- +Write-Host "" +Write-Host "--- DesktopInfo ---" + +Test-Check "Render script exists" { + Test-Path "C:\Windows\Setup\Scripts\DesktopInfo-Render.ps1" +} + +Test-Check "BMP file exists" { + Test-Path "C:\Windows\Setup\Scripts\desktopinfo.bmp" +} -WarnOnly + +# ----------------------------------------------------------------------- +# Summary +# ----------------------------------------------------------------------- +Write-Host "" +Write-Host "========================================" +Write-Host " PASS: $PassCount FAIL: $FailCount WARN: $WarnCount" +Write-Host "========================================" + +if ($FailCount -gt 0) { + Write-Host "Deployment verification FAILED. Review items above." -ForegroundColor Red + exit 1 +} else { + Write-Host "Deployment verification PASSED." -ForegroundColor Green + exit 0 +}