package ws import ( "encoding/json" "fmt" "log" "os/exec" "strings" "time" ) type Message struct { Type string `json:"type"` ID string `json:"id,omitempty"` Command string `json:"command,omitempty"` Status string `json:"status,omitempty"` Error string `json:"error,omitempty"` Params map[string]interface{} `json:"params,omitempty"` // Backend -> Agent Data interface{} `json:"data,omitempty"` // Agent -> Backend (Response) } type Handler struct{} func NewHandler() *Handler { return &Handler{} } func (h *Handler) Handle(raw []byte) []byte { var msg Message if err := json.Unmarshal(raw, &msg); err != nil { log.Printf("WS Parse-Fehler: %v", err) return nil } if msg.Type != "command" { return nil } var resp Message switch msg.Command { case "exec": resp = h.handleExec(msg) case "winget_list": resp = h.handleWingetList(msg) case "winget_install": resp = h.handleWingetInstall(msg) case "winget_upgrade": resp = h.handleWingetUpgrade(msg) case "winget_upgrade_all": resp = h.handleWingetUpgradeAll(msg) case "windows_updates_check": resp = h.handleWinUpdatesCheck(msg) case "windows_updates_install": resp = h.handleWinUpdatesInstall(msg) case "reboot": resp = h.handleReboot(msg) default: resp = Message{Type: "response", ID: msg.ID, Status: "error", Error: fmt.Sprintf("Unbekanntes Command: %s", msg.Command)} } out, _ := json.Marshal(resp) return out } // handleExec führt einen beliebigen Befehl aus (PowerShell oder cmd) func (h *Handler) handleExec(msg Message) Message { resp := Message{Type: "response", ID: msg.ID} data := msg.Params if data == nil { data, _ = msg.Data.(map[string]interface{}) } command, _ := data["command"].(string) if command == "" { resp.Status = "error" resp.Error = "Kein Befehl angegeben" return resp } timeoutSecs := 60 if t, ok := data["timeout"].(float64); ok && t > 0 { timeoutSecs = int(t) } out, err := runWithTimeout(command, timeoutSecs) if err != nil { resp.Status = "error" resp.Error = err.Error() resp.Data = map[string]interface{}{"output": out} } else { resp.Status = "ok" resp.Data = map[string]interface{}{"output": out} } return resp } // handleWingetList listet installierte Software auf func (h *Handler) handleWingetList(msg Message) Message { resp := Message{Type: "response", ID: msg.ID} out, err := runPS("winget list --accept-source-agreements 2>&1") if err != nil { resp.Status = "error" resp.Error = err.Error() return resp } resp.Status = "ok" resp.Data = map[string]interface{}{"output": out} return resp } // handleWingetInstall installiert ein Paket func (h *Handler) handleWingetInstall(msg Message) Message { resp := Message{Type: "response", ID: msg.ID} data := msg.Params if data == nil { data, _ = msg.Data.(map[string]interface{}) } pkg, _ := data["package"].(string) if pkg == "" { resp.Status = "error" resp.Error = "Kein Paket angegeben" return resp } cmd := fmt.Sprintf("winget install --id %s --silent --accept-package-agreements --accept-source-agreements 2>&1", sanitize(pkg)) out, err := runWithTimeout(cmd, 300) if err != nil { resp.Status = "error" resp.Error = err.Error() resp.Data = map[string]interface{}{"output": out} } else { resp.Status = "ok" resp.Data = map[string]interface{}{"output": out} } return resp } // handleWingetUpgrade aktualisiert ein Paket func (h *Handler) handleWingetUpgrade(msg Message) Message { resp := Message{Type: "response", ID: msg.ID} data := msg.Params if data == nil { data, _ = msg.Data.(map[string]interface{}) } pkg, _ := data["package"].(string) if pkg == "" { resp.Status = "error" resp.Error = "Kein Paket angegeben" return resp } cmd := fmt.Sprintf("winget upgrade --id %s --silent --accept-package-agreements --accept-source-agreements 2>&1", sanitize(pkg)) out, err := runWithTimeout(cmd, 300) if err != nil { resp.Status = "error" resp.Error = err.Error() } else { resp.Status = "ok" resp.Data = map[string]interface{}{"output": out} } return resp } // handleWingetUpgradeAll aktualisiert alle Pakete func (h *Handler) handleWingetUpgradeAll(msg Message) Message { resp := Message{Type: "response", ID: msg.ID} out, err := runWithTimeout("winget upgrade --all --silent --accept-package-agreements --accept-source-agreements 2>&1", 600) if err != nil { resp.Status = "error" resp.Error = err.Error() } else { resp.Status = "ok" resp.Data = map[string]interface{}{"output": out} } return resp } // handleWinUpdatesCheck prüft verfügbare Windows-Updates func (h *Handler) handleWinUpdatesCheck(msg Message) Message { resp := Message{Type: "response", ID: msg.ID} script := ` $updateSession = New-Object -ComObject Microsoft.Update.Session $searcher = $updateSession.CreateUpdateSearcher() $result = $searcher.Search("IsInstalled=0 and Type='Software'") $updates = @() foreach ($u in $result.Updates) { $updates += [PSCustomObject]@{ Title = $u.Title KB = ($u.KBArticleIDs | Select-Object -First 1) Size = $u.MaxDownloadSize } } $updates | ConvertTo-Json -Compress` out, err := runPSScript(script, 120) if err != nil { resp.Status = "error" resp.Error = err.Error() return resp } resp.Status = "ok" resp.Data = map[string]interface{}{"raw": out} return resp } // handleWinUpdatesInstall installiert alle ausstehenden Updates func (h *Handler) handleWinUpdatesInstall(msg Message) Message { resp := Message{Type: "response", ID: msg.ID} script := ` $updateSession = New-Object -ComObject Microsoft.Update.Session $searcher = $updateSession.CreateUpdateSearcher() $result = $searcher.Search("IsInstalled=0 and Type='Software'") if ($result.Updates.Count -eq 0) { Write-Output "Keine Updates verfuegbar"; exit 0 } $downloader = $updateSession.CreateUpdateDownloader() $downloader.Updates = $result.Updates $downloader.Download() $installer = $updateSession.CreateUpdateInstaller() $installer.Updates = $result.Updates $installResult = $installer.Install() Write-Output "Installiert: $($result.Updates.Count) Updates, ResultCode: $($installResult.ResultCode)"` out, err := runPSScript(script, 1800) // 30 Min Timeout if err != nil { resp.Status = "error" resp.Error = err.Error() resp.Data = map[string]interface{}{"output": out} } else { resp.Status = "ok" resp.Data = map[string]interface{}{"output": out, "reboot_required": strings.Contains(out, "ResultCode: 2")} } return resp } func (h *Handler) handleReboot(msg Message) Message { go func() { time.Sleep(3 * time.Second) exec.Command("shutdown", "/r", "/t", "10", "/c", "RMM-gesteuert").Run() }() return Message{Type: "response", ID: msg.ID, Status: "ok", Data: map[string]interface{}{"message": "Neustart in 10 Sekunden"}} } // Hilfsfunktionen func runWithTimeout(command string, timeoutSecs int) (string, error) { // PowerShell verwenden — unterstuetzt winget, UTF-8, moderne Windows-Befehle cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-OutputEncoding", "UTF8", "-Command", command) out, err := withTimeout(cmd, time.Duration(timeoutSecs)*time.Second) return out, err } func runPS(command string) (string, error) { cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", command) b, err := cmd.CombinedOutput() return strings.TrimSpace(string(b)), err } func runPSScript(script string, timeoutSecs int) (string, error) { cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", script) out, err := withTimeout(cmd, time.Duration(timeoutSecs)*time.Second) return out, err } func withTimeout(cmd *exec.Cmd, timeout time.Duration) (string, error) { done := make(chan struct{}) var out []byte var err error go func() { out, err = cmd.CombinedOutput() close(done) }() select { case <-done: return strings.TrimSpace(string(out)), err case <-time.After(timeout): cmd.Process.Kill() return "", fmt.Errorf("Timeout nach %v", timeout) } } func sanitize(s string) string { // Einfache Bereinigung — nur alphanumerisch, Punkt, Bindestrich var b strings.Builder for _, r := range s { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '.' || r == '-' || r == '_' { b.WriteRune(r) } } return b.String() }