- 04 profile: pin File Explorer via its AppUserModelID (DesktopApplicationID="Microsoft.Windows.Explorer") instead of a hand-made File Explorer.lnk to explorer.exe. The custom shortcut pinned as a separate app - clicking it launched a second Explorer that did not group with the running window, and the icon could not be unpinned normally. Stop creating that .lnk. - 02 software: install the Atera MSI under NT AUTHORITY\SYSTEM via a one-shot scheduled task (msiexec /qn), then remove the task. Under SYSTEM the agent registers silently with no interactive MFA window, so no technician input is needed. MSI staged in C:\Windows\Temp (readable by SYSTEM). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
286 lines
13 KiB
PowerShell
286 lines
13 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Installs standard business software via winget, sets Adobe PDF default, and installs Atera RMM agent.
|
|
|
|
.DESCRIPTION
|
|
Uses winget to install the standard X9.cz MSP software bundle. Checks winget
|
|
availability before running. Each install is logged. After Adobe Acrobat Reader,
|
|
temporarily stops the UCPD driver (User Choice Protection Driver, present since
|
|
Win11 23H2 / Win10 22H2 update) to allow the HKCR file association write, sets
|
|
.pdf -> AcroRd32, then restarts UCPD. Atera RMM agent is installed for MSP
|
|
monitoring, remote access, and ticketing integration.
|
|
|
|
.ITEMS
|
|
7-zip-7zip-7zip: Installs 7-Zip (winget ID: 7zip.7zip). Used for archive management. Silent install with --accept-package-agreements --accept-source-agreements flags required for unattended deployment.
|
|
adobe-acrobat-reader-64-bit-adobe-acroba: Installs Adobe Acrobat Reader DC 64-bit (Adobe.Acrobat.Reader.64-bit). Required as the default PDF viewer to prevent Edge from handling PDFs in browser mode, which limits functionality.
|
|
openvpn-connect-openvpntechnologies-open: Installs OpenVPN Connect client. Used for client VPN access when the client network requires a VPN. The ovpn profile and credentials are configured separately per client.
|
|
atera-agent-install: Atera RMM agent downloaded from x9.servicedesk.atera.com and installed under NT AUTHORITY\SYSTEM via a one-shot scheduled task (msiexec /qn). Running as SYSTEM registers the agent silently with no interactive MFA window, so no technician input is needed. Agent enables MSP monitoring, remote access, and ticketing integration.
|
|
adobe-pdf-default-pdf-acrord32-po-instal: Sets .pdf -> AcroRd32 file association after Acrobat install via HKCR (system-wide, no UserChoice hash issue). UCPD driver is stopped immediately before the write and restarted after to ensure the association persists across Edge updates.
|
|
ucpd-sys-kernel-driver-od-feb-2024-bloku: UCPD.sys (User Choice Protection Driver) is stopped before the PDF association write and restarted after. Pattern: Stop-Service ucpd -> set HKCR\.pdf -> Start-Service ucpd. Implemented in this script.
|
|
#>
|
|
param(
|
|
[string]$ConfigPath,
|
|
[string]$LogFile
|
|
)
|
|
|
|
. "$PSScriptRoot\common.ps1"
|
|
$Config = Load-Config $ConfigPath
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Check winget availability
|
|
# -----------------------------------------------------------------------
|
|
Write-Log "Checking winget availability" -Level INFO
|
|
|
|
$wingetExe = $null
|
|
$wingetCmd = Get-Command winget -ErrorAction SilentlyContinue
|
|
if ($wingetCmd) {
|
|
$wingetExe = $wingetCmd.Source
|
|
} else {
|
|
# 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) { $wingetExe = $found.FullName; break }
|
|
}
|
|
}
|
|
|
|
if (-not $wingetExe) {
|
|
Write-Log "winget not found - software installation skipped" -Level ERROR
|
|
exit 1
|
|
}
|
|
|
|
Write-Log "winget found: $wingetExe" -Level OK
|
|
|
|
# Accept agreements upfront
|
|
& $wingetExe source update --accept-source-agreements 2>&1 | Out-Null
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Install packages from config
|
|
# -----------------------------------------------------------------------
|
|
if (Get-Feature $Config "software" "wingetInstalls") {
|
|
if (-not $Config -or -not $Config.software -or -not $Config.software.install) {
|
|
Write-Log "No software list in config - skipping installs" -Level WARN
|
|
} else {
|
|
$packages = @($Config.software.install)
|
|
Write-Log "Installing $($packages.Count) packages in parallel" -Level INFO
|
|
|
|
# Launch all winget installs as parallel background jobs
|
|
$jobs = @()
|
|
foreach ($pkg in $packages) {
|
|
Write-Log " Starting: $($pkg.name) ($($pkg.wingetId))" -Level INFO
|
|
$jobs += Start-Job -ArgumentList $pkg.wingetId, $pkg.name, $wingetExe -ScriptBlock {
|
|
param($wingetId, $name, $wingetExe)
|
|
# --source winget forces the winget community repo and bypasses the
|
|
# msstore source, which fails on fresh Win11 ISOs whose App Installer
|
|
# ships a stale pinned cert (0x8a15005e: server certificate did not
|
|
# match any expected values), aborting the install.
|
|
$output = & $wingetExe install --id $wingetId `
|
|
--source winget `
|
|
--silent `
|
|
--accept-package-agreements `
|
|
--accept-source-agreements `
|
|
--disable-interactivity `
|
|
2>&1
|
|
[PSCustomObject]@{
|
|
Name = $name
|
|
WingetId = $wingetId
|
|
ExitCode = $LASTEXITCODE
|
|
Output = ($output -join "`n")
|
|
}
|
|
}
|
|
}
|
|
|
|
# Wait for all jobs and collect results
|
|
Write-Log " Waiting for $($jobs.Count) installs to complete..." -Level INFO
|
|
$jobs | Wait-Job -Timeout 600 | Out-Null
|
|
|
|
# Kill any jobs that are still running after timeout
|
|
foreach ($job in $jobs) {
|
|
if ($job.State -eq "Running") {
|
|
Write-Log " Timeout: $($job.Name) - killing" -Level ERROR
|
|
Stop-Job -Job $job
|
|
}
|
|
}
|
|
|
|
foreach ($job in $jobs) {
|
|
$r = Receive-Job -Job $job
|
|
if ($r.ExitCode -eq 0 -or $r.ExitCode -eq 3010) {
|
|
Write-Log " Installed OK: $($r.Name)" -Level OK
|
|
} elseif ($r.ExitCode -eq -1978335189) {
|
|
Write-Log " Already installed: $($r.Name)" -Level OK
|
|
} else {
|
|
Write-Log " Failed: $($r.Name) (exit $($r.ExitCode))" -Level ERROR
|
|
Write-Log " Output: $($r.Output)" -Level ERROR
|
|
}
|
|
Remove-Job -Job $job
|
|
}
|
|
}
|
|
} else {
|
|
Write-Log "wingetInstalls feature disabled - skipping" -Level INFO
|
|
}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Set Adobe Reader as default PDF app
|
|
# -----------------------------------------------------------------------
|
|
if (Get-Feature $Config "software" "pdfDefault") {
|
|
Write-Log "Setting Adobe Reader as default PDF app" -Level INFO
|
|
|
|
# Stop UCPD driver before writing file association.
|
|
# UCPD.sys (User Choice Protection Driver) blocks UserChoice registry writes
|
|
# on Win11 23H2+ and some Win10 22H2 builds. Stopping it temporarily allows
|
|
# the HKCR association to be written reliably.
|
|
$ucpdStopped = $false
|
|
$ucpdSvc = Get-Service -Name "ucpd" -ErrorAction SilentlyContinue
|
|
if ($ucpdSvc) {
|
|
try {
|
|
Stop-Service -Name "ucpd" -Force -ErrorAction Stop
|
|
# Wait for the service to fully stop before writing association
|
|
Start-Sleep -Seconds 2
|
|
$svcState = (Get-Service -Name "ucpd" -ErrorAction SilentlyContinue).Status
|
|
if ($svcState -eq "Stopped") {
|
|
$ucpdStopped = $true
|
|
Write-Log " UCPD driver stopped" -Level OK
|
|
} else {
|
|
Write-Log " UCPD still in state '$svcState' - association may not persist" -Level WARN
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log " Could not stop UCPD: $_ (association may not persist on some builds)" -Level WARN
|
|
}
|
|
}
|
|
|
|
# Find Adobe PDF viewer executable (Acrobat DC or Reader DC)
|
|
$acroPaths = @(
|
|
"$env:ProgramFiles\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
|
|
"${env:ProgramFiles(x86)}\Adobe\Acrobat DC\Acrobat\Acrobat.exe"
|
|
"${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 " Adobe PDF viewer not found - PDF default not set" -Level WARN
|
|
} else {
|
|
Write-Log " Found: $acroExe" -Level INFO
|
|
|
|
# Mount HKCR PSDrive - not available by default in PS sessions
|
|
if (-not (Get-PSDrive -Name HKCR -ErrorAction SilentlyContinue)) {
|
|
New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT | Out-Null
|
|
}
|
|
|
|
# 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
|
|
|
|
Write-Log " PDF default set to AcroRd32 (HKCR)" -Level OK
|
|
}
|
|
|
|
# Restart UCPD after writing association
|
|
if ($ucpdStopped) {
|
|
try {
|
|
Start-Service -Name "ucpd" -ErrorAction SilentlyContinue
|
|
Write-Log " UCPD driver restarted" -Level OK
|
|
}
|
|
catch {
|
|
Write-Log " Could not restart UCPD: $_" -Level WARN
|
|
}
|
|
}
|
|
} else {
|
|
Write-Log "pdfDefault feature disabled - skipping" -Level INFO
|
|
}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Install Atera RMM Agent
|
|
# Download the MSI from the Atera dashboard API, then install it under the
|
|
# SYSTEM account via a one-shot scheduled task. Under SYSTEM the Atera
|
|
# installer registers silently and does NOT show the interactive MFA window
|
|
# that appears when installing in a user context - so no technician input
|
|
# is needed.
|
|
# -----------------------------------------------------------------------
|
|
if (Get-Feature $Config "software" "ateraAgent") {
|
|
Write-Log "Installing Atera RMM Agent (under SYSTEM)" -Level INFO
|
|
|
|
# Machine-wide temp dir readable by SYSTEM (a per-user TEMP may not be)
|
|
$ateraMsi = "$env:WINDIR\Temp\AteraAgent.msi"
|
|
$ateraUrl = "https://x9.servicedesk.atera.com/api/utils/agent-install/windows/?cid=31&aeid=50b72e7113e54a63ac76b96c54c7e337"
|
|
$ateraTask = "X9-AteraInstall"
|
|
|
|
try {
|
|
Write-Log " Downloading Atera MSI..." -Level INFO
|
|
Invoke-WebRequest -Uri $ateraUrl -OutFile $ateraMsi -UseBasicParsing -ErrorAction Stop
|
|
Write-Log " Download complete" -Level OK
|
|
|
|
# Run msiexec as NT AUTHORITY\SYSTEM via a temporary scheduled task.
|
|
# SYSTEM runs in non-interactive session 0, hence /qn (no UI) and no MFA.
|
|
Write-Log " Installing as SYSTEM via scheduled task (no MFA)..." -Level INFO
|
|
$action = New-ScheduledTaskAction -Execute "msiexec.exe" -Argument "/i `"$ateraMsi`" /qn /norestart"
|
|
$principal = New-ScheduledTaskPrincipal -UserId "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
|
$stTask = New-ScheduledTask -Action $action -Principal $principal
|
|
Register-ScheduledTask -TaskName $ateraTask -InputObject $stTask -Force | Out-Null
|
|
Start-ScheduledTask -TaskName $ateraTask
|
|
|
|
# Wait for the task to finish (msiexec install can take a few minutes)
|
|
$deadline = (Get-Date).AddMinutes(10)
|
|
do {
|
|
Start-Sleep -Seconds 3
|
|
$state = (Get-ScheduledTask -TaskName $ateraTask -ErrorAction SilentlyContinue).State
|
|
} while ($state -eq "Running" -and (Get-Date) -lt $deadline)
|
|
|
|
$result = (Get-ScheduledTaskInfo -TaskName $ateraTask -ErrorAction SilentlyContinue).LastTaskResult
|
|
if ($state -eq "Running") {
|
|
Write-Log " Atera install timed out after 10 min" -Level WARN
|
|
} elseif ($result -eq 0) {
|
|
Write-Log " Atera installer finished (SYSTEM, exit 0)" -Level OK
|
|
} else {
|
|
Write-Log " Atera installer exit code: $result" -Level WARN
|
|
}
|
|
|
|
# Verify install. The AteraAgent service is the most reliable signal -
|
|
# the agent registers it regardless of install path (Atera now sometimes
|
|
# lands under C:\ProgramData instead of Program Files). Fall back to the
|
|
# known binary locations if the service query is inconclusive.
|
|
$ateraSvc = Get-Service -Name "AteraAgent" -ErrorAction SilentlyContinue
|
|
if ($ateraSvc) {
|
|
Write-Log " Atera agent verified (service AteraAgent present, status $($ateraSvc.Status))" -Level OK
|
|
} else {
|
|
$ateraPaths = @(
|
|
"$env:ProgramFiles\ATERA Networks\AteraAgent\AteraAgent.exe"
|
|
"${env:ProgramFiles(x86)}\ATERA Networks\AteraAgent\AteraAgent.exe"
|
|
"$env:ProgramData\ATERA Networks\AteraAgent\AteraAgent.exe"
|
|
)
|
|
if ($ateraPaths | Where-Object { Test-Path $_ }) {
|
|
Write-Log " Atera agent binary verified" -Level OK
|
|
} else {
|
|
Write-Log " Atera agent service and binary not found at expected paths" -Level WARN
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
Write-Log " Atera agent install failed: $_" -Level ERROR
|
|
}
|
|
finally {
|
|
Unregister-ScheduledTask -TaskName $ateraTask -Confirm:$false -ErrorAction SilentlyContinue
|
|
Remove-Item $ateraMsi -ErrorAction SilentlyContinue
|
|
}
|
|
} else {
|
|
Write-Log "ateraAgent feature disabled - skipping" -Level INFO
|
|
}
|
|
|
|
Write-Log "Step 2 complete" -Level OK
|