# ============================================================================= # zurichtech agent installer # Publisher : zurichtech AG # Source : https://get.zurichtech.ch/agent # Support : support@zurichtech.ch # ============================================================================= # Aufrufmuster: # Interaktiv: # iex (irm get.zurichtech.ch/agent) # Unattended (Parameter direkt): # & ([scriptblock]::Create((irm get.zurichtech.ch/agent))) ` # -ClientID 1 -SiteID 1 -AuthKey "abc..." [-AgentType auto] [-Silent] # Unattended (Env-Vars): # $env:ZT_CLIENT='1'; $env:ZT_SITE='1'; $env:ZT_AUTH='abc...' # iex (irm get.zurichtech.ch/agent) # ============================================================================= [CmdletBinding()] param( [int]$ClientID, [int]$SiteID, [string]$AuthKey, [ValidateSet('auto','workstation','server')] [string]$AgentType = 'auto', [switch]$Silent, # unterdrueckt Disclaimer + auto-close [switch]$KeepWindow # Fenster offen lassen auch bei Silent ) # --- Globale Einstellungen --------------------------------------------------- $ErrorActionPreference = "Stop" [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $Host.UI.RawUI.WindowTitle = "zurichtech agent installer" # --- Env-Var-Fallback -------------------------------------------------------- if (-not $ClientID -and $env:ZT_CLIENT) { $ClientID = [int]$env:ZT_CLIENT } if (-not $SiteID -and $env:ZT_SITE) { $SiteID = [int]$env:ZT_SITE } if (-not $AuthKey -and $env:ZT_AUTH) { $AuthKey = $env:ZT_AUTH } if ($env:ZT_SILENT -eq '1') { $Silent = $true } $Unattended = ($ClientID -gt 0 -and $SiteID -gt 0 -and -not [string]::IsNullOrWhiteSpace($AuthKey)) # Diese Werte werden vom Worker zur Request-Zeit injiziert (Option B): $ScriptVersion = "2.2.0" $AgentVersion = "2.10.0" $DownloadUrl = "https://github.com/amidaware/rmmagent/releases/download/v2.10.0/tacticalagent-v2.10.0-windows-amd64.exe" $ApiUrl = "https://api.zurichtech.ch" $LogDir = "C:\ProgramData\zurichtech" $LogFile = Join-Path $LogDir "install.log" $Installer = Join-Path $env:TEMP "tacticalagent.exe" $AgentExe = "C:\Program Files\TacticalAgent\tacticalrmm.exe" # --- Logging-Helfer ---------------------------------------------------------- function Write-Log { param([string]$Message, [string]$Level = "INFO") if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir -Force | Out-Null } $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" "$timestamp [$Level] $Message" | Out-File -FilePath $LogFile -Append -Encoding utf8 } function Write-Step { param([string]$Text) Write-Host "" Write-Host " >> $Text" -ForegroundColor Cyan Write-Log $Text "STEP" } function Write-Ok { param([string]$Text) Write-Host " [OK] $Text" -ForegroundColor Green Write-Log $Text "OK" } function Write-Warn { param([string]$Text) Write-Host " [!] $Text" -ForegroundColor Yellow Write-Log $Text "WARN" } function Write-Err { param([string]$Text) Write-Host " [X] $Text" -ForegroundColor Red Write-Log $Text "ERROR" } # --- Banner ------------------------------------------------------------------ function Write-Banner { Clear-Host Write-Host "" Write-Host " ============================================================" -ForegroundColor Cyan Write-Host " " -ForegroundColor Cyan Write-Host " z u r i c h t e c h a g e n t s e t u p " -ForegroundColor White Write-Host " " -ForegroundColor Cyan Write-Host " ============================================================" -ForegroundColor Cyan Write-Host " Publisher : zurichtech AG " -ForegroundColor Gray Write-Host " Version : $ScriptVersion " -ForegroundColor Gray Write-Host " Source : https://get.zurichtech.ch/agent " -ForegroundColor Gray Write-Host " Support : support@zurichtech.ch " -ForegroundColor Gray Write-Host " ------------------------------------------------------------" -ForegroundColor Cyan Write-Host " Hinweis: Der Auth-Key ist 24 Stunden gueltig. " -ForegroundColor Yellow Write-Host " Nach Ablauf bitte einen neuen Key anfordern. " -ForegroundColor Yellow Write-Host " ============================================================" -ForegroundColor Cyan Write-Host "" } Write-Banner # --- Subtask 2: Self-Elevation ---------------------------------------------- $currentUser = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() $isAdmin = $currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { if ($Unattended) { Write-Host " [X] Unattended-Modus benoetigt bereits Administrator-Rechte." -ForegroundColor Red Write-Host " Starte PowerShell als Administrator und wiederhole den Aufruf." -ForegroundColor Red exit 1 } Write-Host " Administrator-Rechte werden benoetigt." -ForegroundColor Yellow Write-Host " Starte Skript automatisch neu mit erhoehten Rechten ..." -ForegroundColor Yellow try { $scriptUrl = "https://get.zurichtech.ch/agent" $cmd = "iex (irm $scriptUrl)" Start-Process -FilePath "powershell.exe" ` -ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", $cmd ` -Verb RunAs exit 0 } catch { Write-Host " [X] Konnte nicht als Administrator neu starten." -ForegroundColor Red Write-Host " Bitte oeffnen Sie PowerShell manuell als Administrator" -ForegroundColor Red Write-Host " und fuehren Sie folgenden Befehl aus:" -ForegroundColor Red Write-Host "" Write-Host " iex (irm get.zurichtech.ch/agent)" -ForegroundColor White Write-Host "" Read-Host "Druecken Sie [Enter] zum Beenden" exit 1 } } Write-Log "===== Installer gestartet (Version $ScriptVersion) =====" Write-Log "User: $($currentUser.Identity.Name)" Write-Log "Computer: $env:COMPUTERNAME" # --- Subtask 3: Pre-Flight-Checks ------------------------------------------- Write-Step "Pre-Flight-Checks" # OS-Version pruefen try { $os = Get-CimInstance -ClassName Win32_OperatingSystem $osBuild = [int]$os.BuildNumber $osName = $os.Caption Write-Ok "Betriebssystem: $osName (Build $osBuild)" if ($osBuild -lt 14393) { Write-Err "Nicht unterstuetzt. Mindestens Windows 10 / Server 2016 erforderlich." Read-Host "Druecken Sie [Enter] zum Beenden" exit 1 } } catch { Write-Warn "OS-Version konnte nicht ermittelt werden." } # Internet-Zugriff pruefen try { $null = Invoke-WebRequest -Uri $ApiUrl -UseBasicParsing -TimeoutSec 10 -Method Head -ErrorAction Stop Write-Ok "Verbindung zu $ApiUrl moeglich." } catch { Write-Warn "API-Server nicht direkt erreichbar (kann Firewall-Regel sein) - fahre trotzdem fort." } # Bereits installiert? $existingService = Get-Service -Name "tacticalrmm" -ErrorAction SilentlyContinue if ($existingService) { Write-Warn "tacticalrmm-Dienst ist bereits installiert (Status: $($existingService.Status))." $answer = Read-Host " Moechten Sie den Agent NEU installieren? [j/N]" if ($answer -notmatch '^(j|ja|y|yes)$') { Write-Host "" Write-Host " Installation abgebrochen durch Benutzer." -ForegroundColor Yellow Read-Host "Druecken Sie [Enter] zum Beenden" exit 0 } Write-Log "Bestehender Agent wird ueberschrieben." } # Tamper Protection pruefen try { $mp = Get-MpComputerStatus -ErrorAction SilentlyContinue if ($mp -and $mp.IsTamperProtected) { Write-Warn "Windows Defender Tamper Protection ist aktiv - Defender-Ausnahmen koennen ggf. nicht gesetzt werden." } else { Write-Ok "Tamper Protection: nicht aktiv oder Defender nicht praesent." } } catch { Write-Warn "Defender-Status konnte nicht ermittelt werden." } # --- Subtask 4: Sicherheits-Disclaimer -------------------------------------- if (-not $Silent -and -not $Unattended) { Write-Host "" Write-Host " ------------------------------------------------------------" -ForegroundColor DarkGray Write-Host " Was dieses Skript tut:" -ForegroundColor White Write-Host " 1. Setzt Defender-Ausnahmen fuer den Agent-Pfad" -ForegroundColor Gray Write-Host " 2. Laedt den signierten Agent von github.com/amidaware" -ForegroundColor Gray Write-Host " 3. Installiert den Agent unter C:\Program Files\TacticalAgent" -ForegroundColor Gray Write-Host " 4. Registriert das Geraet bei api.zurichtech.ch" -ForegroundColor Gray Write-Host " 5. Schreibt Logs nach $LogFile" -ForegroundColor Gray Write-Host " ------------------------------------------------------------" -ForegroundColor DarkGray Write-Host "" $confirm = Read-Host " Moechten Sie mit der Installation fortfahren? [J/n]" if ($confirm -match '^(n|no|nein)$') { Write-Host "" Write-Host " Installation abgebrochen durch Benutzer." -ForegroundColor Yellow Read-Host "Druecken Sie [Enter] zum Beenden" exit 0 } } # --- Subtask 5: Eingaben mit Validierung ------------------------------------ function Read-NumericInput { param([string]$Prompt) while ($true) { $val = Read-Host " $Prompt" if ($val -match '^\d+$') { return [int]$val } Write-Host " [!] Bitte nur Zahlen eingeben." -ForegroundColor Yellow } } function Read-AuthKey { while ($true) { $secure = Read-Host " 3) Auth-Key (Eingabe wird maskiert)" -AsSecureString $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) $plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) if ($plain.Length -ge 16) { return $plain } Write-Host " [!] Auth-Key zu kurz (mind. 16 Zeichen)." -ForegroundColor Yellow } } if ($Unattended) { Write-Step "Eingaben uebernommen (Unattended-Modus)" Write-Ok "ClientID=$ClientID SiteID=$SiteID AuthKey=*** ($($AuthKey.Length) Zeichen)" } else { Write-Step "Eingaben (vom zurichtech-Support erhalten)" if (-not $ClientID) { $ClientID = Read-NumericInput "1) Client-ID" } if (-not $SiteID) { $SiteID = Read-NumericInput "2) Site-ID" } if (-not $AuthKey) { $AuthKey = Read-AuthKey } } Write-Log "ClientID: $ClientID, SiteID: $SiteID, AuthKey-Laenge: $($AuthKey.Length), Unattended: $Unattended" # --- Subtask 6: AgentType Auto-Detection ------------------------------------ Write-Step "Agent-Typ ermitteln" if ($AgentType -eq 'auto') { try { $productType = (Get-CimInstance Win32_OperatingSystem).ProductType switch ($productType) { 1 { $AgentType = "workstation" } 2 { $AgentType = "server" } # Domain Controller 3 { $AgentType = "server" } # Server default { $AgentType = "workstation" } } Write-Ok "Auto-erkannt: $AgentType (ProductType $productType)" } catch { $AgentType = "workstation" Write-Warn "Konnte ProductType nicht lesen, fallback: $AgentType" } } else { Write-Ok "Per Parameter gesetzt: $AgentType" } # --- Subtask 7: Defender-Exclusions ----------------------------------------- Write-Step "Windows-Defender-Ausnahmen setzen" $excludePaths = @( 'C:\Program Files\TacticalAgent', 'C:\Program Files\TacticalAgent\*', 'C:\Program Files\Mesh Agent', 'C:\Program Files\Mesh Agent\*', 'C:\ProgramData\TacticalRMM', 'C:\ProgramData\TacticalRMM\*', "$env:TEMP\tacticalagent.exe" ) $excludeProcs = @( 'tacticalrmm.exe', 'tacticalagent.exe', 'meshagent.exe', 'MeshAgent.exe' ) $exclusionFailures = 0 foreach ($p in $excludePaths) { try { Add-MpPreference -ExclusionPath $p -ErrorAction Stop } catch { $exclusionFailures++ } } foreach ($pr in $excludeProcs) { try { Add-MpPreference -ExclusionProcess $pr -ErrorAction Stop } catch { $exclusionFailures++ } } if ($exclusionFailures -eq 0) { Write-Ok "Defender-Ausnahmen erfolgreich gesetzt." } else { Write-Warn "$exclusionFailures Ausnahme(n) konnten nicht gesetzt werden (Tamper Protection oder Drittanbieter-AV?)." } # --- Subtask 8: Download (Quelle: GitHub Releases - immutable) ------------- Write-Step "Agent v$AgentVersion herunterladen" Write-Host " Quelle: $DownloadUrl" -ForegroundColor DarkGray try { if (Test-Path $Installer) { Remove-Item $Installer -Force } Invoke-WebRequest -Uri $DownloadUrl -OutFile $Installer -UseBasicParsing $sizeMB = [math]::Round((Get-Item $Installer).Length/1MB,2) $sha = (Get-FileHash -Path $Installer -Algorithm SHA256).Hash Write-Ok "Download abgeschlossen ($sizeMB MB)." Write-Host " SHA256: $sha" -ForegroundColor DarkGray Write-Log "Downloaded $($Installer) size=$sizeMB MB sha256=$sha" } catch { Write-Err "Download fehlgeschlagen: $($_.Exception.Message)" Read-Host "Druecken Sie [Enter] zum Beenden" exit 1 } # --- Subtask 9: Installation & Registrierung ------------------------------- Write-Step "Agent installieren" try { $proc = Start-Process -FilePath $Installer -ArgumentList "/VERYSILENT", "/SUPPRESSMSGBOXES" -Wait -NoNewWindow -PassThru if ($proc.ExitCode -ne 0) { Write-Warn "Installer Exit-Code: $($proc.ExitCode)" } Write-Ok "Setup abgeschlossen." } catch { Write-Err "Installation fehlgeschlagen: $($_.Exception.Message)" Read-Host "Druecken Sie [Enter] zum Beenden" exit 1 } Start-Sleep -Seconds 5 if (-not (Test-Path $AgentExe)) { Write-Err "Agent-Datei nicht gefunden unter $AgentExe." Read-Host "Druecken Sie [Enter] zum Beenden" exit 1 } Write-Step "Agent bei api.zurichtech.ch registrieren" $RegisterArgs = @( "-m", "install", "--api", $ApiUrl, "--client-id", $ClientID, "--site-id", $SiteID, "--agent-type", $AgentType, "--auth", $AuthKey, "--ping", "--silent" ) # Hintergrund-Job: Schliesst die "Tactical RMM"-MessageBox, falls der # Agent das --silent-Flag nicht respektiert (aeltere Versionen). $popupKiller = Start-Job -ScriptBlock { Add-Type -Namespace W -Name U -MemberDefinition @' [System.Runtime.InteropServices.DllImport("user32.dll")] public static extern int FindWindow(string lpClassName, string lpWindowName); [System.Runtime.InteropServices.DllImport("user32.dll")] public static extern int PostMessage(int hWnd, uint Msg, int wParam, int lParam); '@ $deadline = (Get-Date).AddSeconds(120) while ((Get-Date) -lt $deadline) { foreach ($title in @('Tactical RMM','TacticalRMM','Tactical Agent')) { $hwnd = [W.U]::FindWindow($null, $title) if ($hwnd -ne 0) { [void][W.U]::PostMessage($hwnd, 0x0010, 0, 0) } # WM_CLOSE } Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowTitle -match '^Tactical' } | ForEach-Object { try { $_.CloseMainWindow() | Out-Null } catch {} } Start-Sleep -Milliseconds 250 } } try { $regProc = Start-Process -FilePath $AgentExe -ArgumentList $RegisterArgs -Wait -NoNewWindow -PassThru if ($regProc.ExitCode -eq 0) { Write-Ok "Registrierung erfolgreich." } else { Write-Warn "Registrierung Exit-Code: $($regProc.ExitCode) (Auth-Key ggf. abgelaufen?)." } } catch { Write-Err "Registrierung fehlgeschlagen: $($_.Exception.Message)" } finally { Start-Sleep -Seconds 2 Stop-Job -Job $popupKiller -ErrorAction SilentlyContinue | Out-Null Remove-Job -Job $popupKiller -Force -ErrorAction SilentlyContinue | Out-Null } # Auth-Key sofort aus Speicher loeschen $AuthKey = $null [GC]::Collect() # --- Subtask 10: Post-Install-Verifizierung --------------------------------- Write-Step "Verifizierung" $svc = Get-Service -Name "tacticalrmm" -ErrorAction SilentlyContinue if ($svc) { if ($svc.Status -ne "Running") { try { Start-Service -Name "tacticalrmm" -ErrorAction Stop } catch {} Start-Sleep -Seconds 3 $svc = Get-Service -Name "tacticalrmm" } Write-Ok "Dienst tacticalrmm: $($svc.Status)" } else { Write-Err "Dienst tacticalrmm nicht gefunden." } $meshSvc = Get-Service -Name "Mesh Agent" -ErrorAction SilentlyContinue if ($meshSvc) { Write-Ok "Dienst Mesh Agent: $($meshSvc.Status)" } else { Write-Warn "Dienst 'Mesh Agent' nicht gefunden (wird ggf. spaeter automatisch installiert)." } # --- Subtask 11: Cleanup & Summary ------------------------------------------ Write-Step "Aufraeumen" if (Test-Path $Installer) { try { Remove-Item -Path $Installer -Force Write-Ok "Installer-Datei entfernt." } catch { Write-Warn "Installer konnte nicht entfernt werden." } } Write-Log "===== Installer beendet =====" Write-Host "" Write-Host " ============================================================" -ForegroundColor Green Write-Host " Installation abgeschlossen" -ForegroundColor Green Write-Host " ============================================================" -ForegroundColor Green Write-Host " Computer : $env:COMPUTERNAME" -ForegroundColor Gray Write-Host " Agent-Typ : $AgentType" -ForegroundColor Gray Write-Host " Client / Site: $ClientID / $SiteID" -ForegroundColor Gray Write-Host " Logfile : $LogFile" -ForegroundColor Gray Write-Host " ------------------------------------------------------------" -ForegroundColor Green Write-Host " Bei Fragen: support@zurichtech.ch" -ForegroundColor White Write-Host " ============================================================" -ForegroundColor Green Write-Host "" if ($KeepWindow) { Read-Host "Druecken Sie [Enter] zum Beenden" } else { Write-Host " Fenster schliesst in 5 Sekunden ..." -ForegroundColor DarkGray Start-Sleep -Seconds 5 Stop-Process -Id $PID -Force }