diff --git a/.gitignore b/.gitignore index 7c75b49..2bf8052 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,44 @@ -build/ -rmm-agent -rmm-backend -*.db -*.log -*.pid +# ======================== +# Konfigurationen mit Passwörtern — NIEMALS einchecken +# ======================== config.yaml -beispielbilder/ -certs/ -opnsense-plugin/stage/ -opnsense-plugin/*.pkg -frontend/node_modules/ -frontend/dist/ agent/config.yaml backend/config.yaml -frontend/src/config.js +opnsense-plugin/src/opnsense/service/templates/OPNsense/RMMAgent/config.yaml + +# Beispiel-Configs sind erlaubt +!config.yaml.example +!**/config.yaml.example + +# ======================== +# Zertifikate & Secrets +# ======================== +certs/ +*.pem +*.key +*.crt +*.p12 + +# ======================== +# Kompilierte Binaries +# ======================== +backend/rmm-backend +agent/rmm-agent +agent-linux/rmm-agent +build/ +updater/rmm-updater + +# ======================== +# Frontend Build +# ======================== +frontend/node_modules/ +frontend/dist/ + +# ======================== +# OS / Editor +# ======================== +.DS_Store +*.swp +*.swo +.idea/ +.vscode/ diff --git a/agent/collector/caddy.go b/agent/collector/caddy.go index 34ba892..8b36b79 100644 --- a/agent/collector/caddy.go +++ b/agent/collector/caddy.go @@ -1,9 +1,13 @@ package collector import ( + "encoding/json" + "io" + "net/http" "os" "os/exec" "strings" + "time" ) type CaddySite struct { @@ -12,12 +16,21 @@ type CaddySite struct { TLS bool `json:"tls"` } +type CaddyUpstream struct { + Address string `json:"address"` + NumRequests int `json:"num_requests"` + Fails int `json:"fails"` + Healthy bool `json:"healthy"` +} + type CaddyData struct { - Version string `json:"version"` - Sites []CaddySite `json:"sites"` + Version string `json:"version"` + Sites []CaddySite `json:"sites"` + Upstreams []CaddyUpstream `json:"upstreams,omitempty"` } const caddyFile = "/usr/local/etc/caddy/Caddyfile" +const caddyAdminAPI = "http://127.0.0.1:2019" func CollectCaddy() *CaddyData { if _, err := os.Stat(caddyFile); err != nil { @@ -34,7 +47,7 @@ func CollectCaddy() *CaddyData { } } - // Parse Caddyfile + // Parse Caddyfile fuer Sites content, err := os.ReadFile(caddyFile) if err != nil { return data @@ -51,7 +64,6 @@ func CollectCaddy() *CaddyData { continue } - // Check for domain block start: "domain.com {" or "http://domain.com {" if depth == 0 && strings.Contains(trimmed, "{") { parts := strings.Fields(trimmed) if len(parts) >= 2 && parts[len(parts)-1] == "{" { @@ -63,7 +75,6 @@ func CollectCaddy() *CaddyData { } if depth > 0 { - // Track nested braces depth += strings.Count(trimmed, "{") - strings.Count(trimmed, "}") if strings.HasPrefix(trimmed, "reverse_proxy") { @@ -85,5 +96,43 @@ func CollectCaddy() *CaddyData { } } + // Admin-API: Upstream-Statistiken + data.Upstreams = collectCaddyUpstreams() + return data } + +func collectCaddyUpstreams() []CaddyUpstream { + client := &http.Client{Timeout: 3 * time.Second} + resp, err := client.Get(caddyAdminAPI + "/api/reverse_proxy/upstreams") + if err != nil { + return nil + } + defer resp.Body.Close() + + body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) + if err != nil { + return nil + } + + // Admin-API liefert: [{"address":"host:port","num_requests":N,"fails":N}] + var raw []struct { + Address string `json:"address"` + NumRequests int `json:"num_requests"` + Fails int `json:"fails"` + } + if err := json.Unmarshal(body, &raw); err != nil { + return nil + } + + upstreams := make([]CaddyUpstream, 0, len(raw)) + for _, u := range raw { + upstreams = append(upstreams, CaddyUpstream{ + Address: u.Address, + NumRequests: u.NumRequests, + Fails: u.Fails, + Healthy: u.Fails == 0, + }) + } + return upstreams +} diff --git a/backend/api/firmware.go b/backend/api/firmware.go index 3b477e1..8d6971f 100644 --- a/backend/api/firmware.go +++ b/backend/api/firmware.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "log" + "strings" "net/http" "github.com/cynfo/rmm-backend/ws" @@ -32,12 +33,32 @@ func (h *Handler) uploadFirmware(w http.ResponseWriter, r *http.Request) { return } - // Binary aus Body lesen (max 50MB) + // Binary lesen — Multipart (curl -F) oder raw Body (Content-Type: application/octet-stream) r.Body = http.MaxBytesReader(w, r.Body, 50*1024*1024) - binary, err := io.ReadAll(r.Body) - if err != nil { - writeError(w, http.StatusBadRequest, "Binary lesen fehlgeschlagen") - return + var binary []byte + if ct := r.Header.Get("Content-Type"); strings.HasPrefix(ct, "multipart/") { + if err := r.ParseMultipartForm(50 << 20); err != nil { + writeError(w, http.StatusBadRequest, "Multipart-Parse fehlgeschlagen") + return + } + file, _, err := r.FormFile("file") + if err != nil { + writeError(w, http.StatusBadRequest, "Feld 'file' fehlt im Multipart-Form") + return + } + defer file.Close() + binary, err = io.ReadAll(file) + if err != nil { + writeError(w, http.StatusBadRequest, "Binary lesen fehlgeschlagen") + return + } + } else { + var err error + binary, err = io.ReadAll(r.Body) + if err != nil { + writeError(w, http.StatusBadRequest, "Binary lesen fehlgeschlagen") + return + } } if len(binary) == 0 { diff --git a/backend/api/handlers.go b/backend/api/handlers.go index f678956..7398843 100644 --- a/backend/api/handlers.go +++ b/backend/api/handlers.go @@ -16,7 +16,8 @@ import ( ) type Handler struct { - db *db.Database + db *db.Database + scheduler *TaskScheduler } func NewHandler(database *db.Database) *Handler { @@ -86,7 +87,9 @@ func (h *Handler) SetupRoutes(mux *http.ServeMux, hub *ws.Hub) { mux.HandleFunc("POST /api/v1/tasks", h.createTask(hub)) mux.HandleFunc("DELETE /api/v1/tasks/{id}", h.deleteTask) mux.HandleFunc("POST /api/v1/tasks/{id}/cancel", h.cancelTask) + mux.HandleFunc("PUT /api/v1/tasks/{id}", h.updateTask) mux.HandleFunc("PUT /api/v1/tasks/{id}/toggle", h.toggleTask) + mux.HandleFunc("POST /api/v1/tasks/{id}/run", h.runTaskNow()) // WebSocket und Tunnel-Routes h.setupTunnelRoutes(mux, hub) diff --git a/backend/api/scheduler.go b/backend/api/scheduler.go index fea1e59..5efb521 100644 --- a/backend/api/scheduler.go +++ b/backend/api/scheduler.go @@ -275,8 +275,56 @@ func (s *TaskScheduler) executeUpdateWithHealthCheck(agentID string, reboot bool return result, nil } -func (s *TaskScheduler) sendWebhook(taskID int, url string, task interface{}) { - payload, err := json.Marshal(task) +// WebhookPayload ist der strukturierte Payload fuer ERP-Serviceberichte +type WebhookPayload struct { + CustomerID string `json:"customer_id"` + Hostname string `json:"hostname"` + OPNsenseVersion string `json:"opnsense_version"` + Timestamp string `json:"timestamp"` + Trigger string `json:"trigger"` + Reachable bool `json:"reachable"` + UpdateStatus string `json:"update_status"` +} + +func (s *TaskScheduler) sendWebhook(taskID int, url string, task *models.Task) { + // Agent-Daten aus DB holen (hostname, opnsense_version) + agent, _, err := s.db.GetAgent(task.AgentID) + if err != nil || agent == nil { + log.Printf("Scheduler: Webhook Task #%d — Agent %s nicht gefunden: %v", taskID, task.AgentID, err) + s.db.SetTaskWebhookResult(taskID, false, fmt.Sprintf("Agent nicht gefunden: %v", err)) + return + } + + // Erreichbarkeit aus Health-Check-Result extrahieren + reachable := true + if result, ok := task.Result.(map[string]interface{}); ok { + if hc, ok := result["health_check"].(map[string]interface{}); ok { + if online, ok := hc["agent_online"].(bool); ok { + reachable = online + } + } + } + + // Trigger aus Action ableiten + trigger := task.Action + "_completed" + + // Timestamp: FinishedAt oder jetzt + timestamp := time.Now().UTC().Format(time.RFC3339) + if task.FinishedAt != nil && *task.FinishedAt != "" { + timestamp = *task.FinishedAt + } + + payload := WebhookPayload{ + CustomerID: task.CustomerNumber, + Hostname: agent.Hostname, + OPNsenseVersion: agent.OPNsenseVersion, + Timestamp: timestamp, + Trigger: trigger, + Reachable: reachable, + UpdateStatus: task.Status, + } + + payloadJSON, err := json.Marshal(payload) if err != nil { log.Printf("Scheduler: Webhook-Payload fuer Task #%d serialisieren fehlgeschlagen: %v", taskID, err) s.db.SetTaskWebhookResult(taskID, false, fmt.Sprintf("JSON-Fehler: %v", err)) @@ -284,7 +332,7 @@ func (s *TaskScheduler) sendWebhook(taskID int, url string, task interface{}) { } client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Post(url, "application/json", bytes.NewReader(payload)) + resp, err := client.Post(url, "application/json", bytes.NewReader(payloadJSON)) if err != nil { log.Printf("Scheduler: Webhook fuer Task #%d fehlgeschlagen: %v", taskID, err) s.db.SetTaskWebhookResult(taskID, false, fmt.Sprintf("Fehler: %v", err)) diff --git a/backend/api/tasks.go b/backend/api/tasks.go index 3080b58..c0bf497 100644 --- a/backend/api/tasks.go +++ b/backend/api/tasks.go @@ -180,6 +180,90 @@ func (h *Handler) cancelTask(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"message": "Task abgebrochen"}) } +// PUT /api/v1/tasks/{id} +func (h *Handler) updateTask(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + id, err := strconv.Atoi(idStr) + if err != nil { + writeError(w, http.StatusBadRequest, "Ungueltige Task-ID") + return + } + + var req struct { + Name string `json:"name"` + ScheduleType string `json:"schedule_type"` + ScheduleTime string `json:"schedule_time"` + ScheduleWeekday *int `json:"schedule_weekday"` + ScheduleMonthday *int `json:"schedule_monthday"` + Reboot bool `json:"reboot"` + HealthCheck bool `json:"health_check"` + HealthCheckTimeout int `json:"health_check_timeout"` + WebhookURL string `json:"webhook_url"` + Active *bool `json:"active"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "Ungueltige Anfrage") + return + } + + active := true + if req.Active != nil { + active = *req.Active + } + if req.HealthCheckTimeout <= 0 { + req.HealthCheckTimeout = 600 + } + + task, err := h.db.UpdateTask(id, req.Name, req.ScheduleType, req.ScheduleTime, + req.ScheduleWeekday, req.ScheduleMonthday, + req.Reboot, req.HealthCheck, req.HealthCheckTimeout, req.WebhookURL, active) + if err != nil { + writeError(w, http.StatusInternalServerError, "Fehler: "+err.Error()) + return + } + if task == nil { + writeError(w, http.StatusNotFound, "Task nicht gefunden") + return + } + + h.auditLog(r, "task.update", "task", idStr, "", "") + writeJSON(w, http.StatusOK, task) +} + +// SetScheduler setzt den Scheduler im Handler (fuer Run-Now) +func (h *Handler) SetScheduler(s *TaskScheduler) { + h.scheduler = s +} + +// POST /api/v1/tasks/{id}/run — Task sofort ausfuehren +func (h *Handler) runTaskNow() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + id, err := strconv.Atoi(idStr) + if err != nil { + writeError(w, http.StatusBadRequest, "Ungueltige Task-ID") + return + } + + task, err := h.db.GetTask(id) + if err != nil || task == nil { + writeError(w, http.StatusNotFound, "Task nicht gefunden") + return + } + + if h.scheduler == nil { + writeError(w, http.StatusInternalServerError, "Scheduler nicht verfuegbar") + return + } + + // Sofort im Hintergrund starten + go h.scheduler.executeTask(*task) + + h.auditLog(r, "task.run_now", "task", idStr, "", task.Action) + writeJSON(w, http.StatusOK, map[string]string{"message": "Task gestartet"}) + } +} + // DELETE /api/v1/tasks/{id} func (h *Handler) deleteTask(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") diff --git a/backend/db/tasks.go b/backend/db/tasks.go index 4bb156f..13546f4 100644 --- a/backend/db/tasks.go +++ b/backend/db/tasks.go @@ -422,6 +422,27 @@ func (d *Database) UpdateTaskStatusDirect(id int, status string) error { return err } +func (d *Database) UpdateTask(id int, name, scheduleType, scheduleTime string, weekday, monthday *int, + reboot, healthCheck bool, healthCheckTimeout int, webhookURL string, active bool) (*models.Task, error) { + + now := time.Now() + nextRun := CalculateNextRun(scheduleType, scheduleTime, weekday, monthday, now) + + _, err := d.db.Exec(` + UPDATE tasks SET + name = $1, schedule_type = $2, schedule_time = $3, + schedule_weekday = $4, schedule_monthday = $5, + reboot = $6, health_check = $7, health_check_timeout = $8, + webhook_url = $9, active = $10, next_run = $11 + WHERE id = $12 + `, name, scheduleType, scheduleTime, weekday, monthday, + reboot, healthCheck, healthCheckTimeout, webhookURL, active, nextRun, id) + if err != nil { + return nil, fmt.Errorf("Task aktualisieren: %w", err) + } + return d.GetTask(id) +} + func (d *Database) DeleteTask(id int) error { res, err := d.db.Exec("DELETE FROM tasks WHERE id = $1", id) if err != nil { diff --git a/backend/main.go b/backend/main.go index 0741b19..bf80ff3 100644 --- a/backend/main.go +++ b/backend/main.go @@ -78,6 +78,7 @@ func main() { // Geschuetzte Routes protectedMux := http.NewServeMux() handler := api.NewHandler(database) + handler.SetScheduler(scheduler) handler.SetupRoutes(protectedMux, hub) // GET /api/v1/auth/me braucht JWT protectedMux.HandleFunc("GET /api/v1/auth/me", authHandler.Me) diff --git a/backend/models/system.go b/backend/models/system.go index e47f989..64c5a45 100644 --- a/backend/models/system.go +++ b/backend/models/system.go @@ -264,9 +264,17 @@ type CaddySite struct { TLS bool `json:"tls"` } +type CaddyUpstream struct { + Address string `json:"address"` + NumRequests int `json:"num_requests"` + Fails int `json:"fails"` + Healthy bool `json:"healthy"` +} + type CaddyData struct { - Version string `json:"version"` - Sites []CaddySite `json:"sites"` + Version string `json:"version"` + Sites []CaddySite `json:"sites"` + Upstreams []CaddyUpstream `json:"upstreams,omitempty"` } // HeartbeatRequest diff --git a/beispielbilder/Firewall_sec_wazuh.png b/beispielbilder/Firewall_sec_wazuh.png new file mode 100644 index 0000000..0fa4a55 Binary files /dev/null and b/beispielbilder/Firewall_sec_wazuh.png differ diff --git a/beispielbilder/Lastenheft b/beispielbilder/Lastenheft new file mode 100644 index 0000000..1331861 --- /dev/null +++ b/beispielbilder/Lastenheft @@ -0,0 +1,221 @@ +# Lastenheft – OPNsense Zentralmanagement Agent + +## 1. Zielsetzung + +Ziel dieses Projektes ist die Entwicklung eines **OPNsense Zentralmanagement-Agents**, der es einer externen Management‑ und Monitoring‑Plattform ermöglicht, weitreichende Systeminformationen auszulesen sowie definierte Systemaktionen und Änderungen auf OPNsense‑Firewalls durchzuführen. + +Da die bestehende OPNsense‑API für den vorgesehenen Funktionsumfang nicht ausreichend ist, soll ein **Agent-basiertes Architekturmodell** umgesetzt werden. Der Agent stellt eine sichere, persistente Verbindung zu einem zentralen Backend her und fungiert als Kontroll‑, Informations‑ und Tunnel‑Endpunkt. + +im Ordner beispielbilder sind vergleichbare Portale + +## 2. Ausgangssituation + +* Mehrere OPNsense‑Firewalls (verschiedene Versionen, Standorte, Kunden) +* Zentrale Verwaltungsoberfläche (ähnlich den gezeigten UI‑Mockups) +* Bestehende OPNsense‑API liefert nicht alle benötigten Informationen +* Bedarf an: + + * System‑Transparenz + * Fernzugriff (SSH / WebUI) + * Zentralem Backup‑ und Zertifikatsmanagement + +## 3. Gesamtarchitektur + +### 3.1 Komponenten + +#### 3.1.1 OPNsense Agent (pro Firewall) + +* Läuft lokal auf der OPNsense (Plugin oder Service) +* Baut **initiativ** eine Verbindung zum Backend auf (Outbound only) +* Liefert Systemdaten +* Führt Kommandos und Aktionen kontrolliert aus + +#### 3.1.2 Backend Server (Control Plane) + +* Zentrale Kommunikations‑ und Steuerinstanz +* Hält persistente Agent‑Verbindungen +* Stellt APIs für das Frontend bereit +* Baut Tunnel (SSH / HTTPS) zu den Firewalls über den Agent auf + +#### 3.1.3 Frontend Server (UI / Webinterface) + +* Web‑UI für Administratoren +* Darstellung der Firewalls (Übersichten, Details, Status) +* Steuerung von Aktionen (Tunnel, Backups, Reloads) + +## 4. Funktionale Anforderungen – Agent + +### 4.1 Hardware‑ und Systeminformationen + +Der Agent muss folgende Informationen bereitstellen: + +* Hersteller +* Modell +* Seriennummer +* BIOS‑Version +* CPU (Typ, Kerne, Threads, Takt) +* RAM (gesamt, frei) +* Massenspeicher (Typ, Kapazität, Belegung) +* OPNsense‑Version +* Uptime + +### 4.2 Dienste und Status + +Der Agent muss: + +* Alle Systemdienste auflisten +* Status je Dienst liefern (running / stopped) +* Dienst neu starten / stoppen / starten können (optional konfigurierbar) + +Beispiele: + +* configd +* dhcpd / kea +* unbound +* openvpn +* wireguard +* cron + +### 4.3 Netzwerk‑Interfaces + +Der Agent muss: + +* Alle Interfaces anzeigen +* Rolle (WAN / LAN / OPT) +* IP‑Adresse(n) +* MAC‑Adresse +* RX/TX‑Traffic +* Status (up/down) + +### 4.4 VPN‑Informationen + +#### 4.4.1 WireGuard + +* Tunnelname +* Peers +* Status (up/down) +* Transferstatistiken + +#### 4.4.2 OpenVPN + +* Server / Client‑Instanzen +* Verbindungsstatus +* Road‑Warrior‑Sessions + +### 4.5 Routing + +* Anzeige der Routing‑Tabelle +* Zielnetz +* Gateway +* Interface +* Typ (statisch / dynamisch) + +### 4.6 DHCP (KEA) + +* Aktive DHCP‑Instanzen +* Pools +* Leases (IPv4 / IPv6) +* Lease‑Status +* Zuordnung MAC ↔ IP + +### 4.7 Zertifikate + +* Auflistung aller Zertifikate +* Typ (CA, Server, User, ACME) +* Gültigkeitszeitraum +* Ablaufwarnungen +* Zertifikat neu laden / erneuern (optional) + +### 4.8 Backups + +Der Agent muss: + +* Konfigurations‑Backups erstellen können +* Backup lokal zwischenspeichern +* Backup an Backend übertragen +* Metadaten liefern (Größe, Zeitstempel) + +## 5. Tunnel‑Funktionalität + +### 5.1 Ziel + +Das Backend soll on‑demand Tunnel zu einer Firewall aufbauen können, ohne dass direkte eingehende Verbindungen zur Firewall notwendig sind. + +### 5.2 Tunnelarten + +* SSH‑Tunnel +* HTTPS‑Tunnel (WebUI) + +### 5.3 Funktionsweise + +* Agent hält persistente Verbindung zum Backend +* Backend fordert Tunnel an +* Agent öffnet lokalen Zielport (z. B. 127.0.0.1:22 oder 443) +* Agent erstellt Reverse‑Tunnel +* Backend stellt öffentlichen / internen Zugriffspunkt bereit + +### 5.4 Anforderungen an Tunnel + +* Vergleichbar mit **stunnel** +* Konfigurierbar: + + * Zielport + * Protokoll + * Laufzeit + * Zugriffsbeschränkung +* Frontend zeigt klickbaren Link zum Tunnel + +## 6. Sicherheit & Compliance + +### 6.1 Kommunikation + +* TLS‑verschlüsselt (mTLS bevorzugt) +* Agent authentifiziert sich eindeutig +* Zertifikatsbasierte Identität + +### 6.2 Rechte & Rollen + +* Agent darf nur explizit erlaubte Aktionen ausführen +* Rollenmodell im Backend (Admin, Read‑Only, Operator) + +### 6.3 Logging & Audit + +* Alle Aktionen werden protokolliert +* Tunnel‑Nutzung wird geloggt +* Konfigurationsänderungen nachvollziehbar + +## 7. Nicht‑funktionale Anforderungen + +### 7.1 Performance + +* Geringe Last auf OPNsense +* Asynchrone Abfragen + +### 7.2 Stabilität + +* Agent überlebt OPNsense‑Updates +* Automatische Reconnect‑Logik + +### 7.3 Skalierbarkeit + +* Unterstützung für viele hundert Firewalls +* Mandantenfähigkeit (Kunden / Standorte) + +## 8. Abgrenzung + +Nicht Bestandteil dieses Lastenhefts: + +* Vollständige Firewall‑Rule‑Bearbeitung +* Deep Packet Inspection +* IDS/IPS‑Regelverwaltung + +## 9. Abnahmekriterien + +* Alle genannten Informationspunkte sind im Frontend sichtbar +* Tunnel können on‑demand aufgebaut und beendet werden +* Backups sind abrufbar und wiederherstellbar +* Agent funktioniert mit mehreren OPNsense‑Versionen + +--- + +*Dieses Lastenheft basiert auf den bereitgestellten Screenshots, der beschriebenen Zielarchitektur und den funktionalen Anforderungen des Auftraggebers.* diff --git a/beispielbilder/firewal_dienste.png b/beispielbilder/firewal_dienste.png new file mode 100644 index 0000000..c21eca7 Binary files /dev/null and b/beispielbilder/firewal_dienste.png differ diff --git a/beispielbilder/firewall_agent.png b/beispielbilder/firewall_agent.png new file mode 100644 index 0000000..e916891 Binary files /dev/null and b/beispielbilder/firewall_agent.png differ diff --git a/beispielbilder/firewall_backup.png b/beispielbilder/firewall_backup.png new file mode 100644 index 0000000..c9905ba Binary files /dev/null and b/beispielbilder/firewall_backup.png differ diff --git a/beispielbilder/firewall_check.png b/beispielbilder/firewall_check.png new file mode 100644 index 0000000..3dd4561 Binary files /dev/null and b/beispielbilder/firewall_check.png differ diff --git a/beispielbilder/firewall_dhcp.png b/beispielbilder/firewall_dhcp.png new file mode 100644 index 0000000..9687cef Binary files /dev/null and b/beispielbilder/firewall_dhcp.png differ diff --git a/beispielbilder/firewall_interface.png b/beispielbilder/firewall_interface.png new file mode 100644 index 0000000..826366b Binary files /dev/null and b/beispielbilder/firewall_interface.png differ diff --git a/beispielbilder/firewall_loschen.png b/beispielbilder/firewall_loschen.png new file mode 100644 index 0000000..b0f9d75 Binary files /dev/null and b/beispielbilder/firewall_loschen.png differ diff --git a/beispielbilder/firewall_portal_log.png b/beispielbilder/firewall_portal_log.png new file mode 100644 index 0000000..db32b65 Binary files /dev/null and b/beispielbilder/firewall_portal_log.png differ diff --git a/beispielbilder/firewall_routen.png b/beispielbilder/firewall_routen.png new file mode 100644 index 0000000..a270ac6 Binary files /dev/null and b/beispielbilder/firewall_routen.png differ diff --git a/beispielbilder/firewall_shell.png b/beispielbilder/firewall_shell.png new file mode 100644 index 0000000..9f302f7 Binary files /dev/null and b/beispielbilder/firewall_shell.png differ diff --git a/beispielbilder/firewall_tunnel.png b/beispielbilder/firewall_tunnel.png new file mode 100644 index 0000000..664e432 Binary files /dev/null and b/beispielbilder/firewall_tunnel.png differ diff --git a/beispielbilder/firewall_ubersicht.png b/beispielbilder/firewall_ubersicht.png new file mode 100644 index 0000000..2fc086a Binary files /dev/null and b/beispielbilder/firewall_ubersicht.png differ diff --git a/beispielbilder/firewall_update.png b/beispielbilder/firewall_update.png new file mode 100644 index 0000000..f9461b7 Binary files /dev/null and b/beispielbilder/firewall_update.png differ diff --git a/beispielbilder/firewall_vpn.png b/beispielbilder/firewall_vpn.png new file mode 100644 index 0000000..ae6856c Binary files /dev/null and b/beispielbilder/firewall_vpn.png differ diff --git a/beispielbilder/firewall_wazuh.png b/beispielbilder/firewall_wazuh.png new file mode 100644 index 0000000..f62dbc9 Binary files /dev/null and b/beispielbilder/firewall_wazuh.png differ diff --git a/beispielbilder/firewall_zertifikate.png b/beispielbilder/firewall_zertifikate.png new file mode 100644 index 0000000..e882a68 Binary files /dev/null and b/beispielbilder/firewall_zertifikate.png differ diff --git a/beispielbilder/firewalljob.png b/beispielbilder/firewalljob.png new file mode 100644 index 0000000..6adfec1 Binary files /dev/null and b/beispielbilder/firewalljob.png differ diff --git a/beispielbilder/startseite.png b/beispielbilder/startseite.png new file mode 100644 index 0000000..6af3166 Binary files /dev/null and b/beispielbilder/startseite.png differ diff --git a/deploy-frontend.sh b/deploy-frontend.sh new file mode 100755 index 0000000..31e2b05 --- /dev/null +++ b/deploy-frontend.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e +FRONTEND_DIR="$(dirname "$0")/frontend" +REMOTE="root@192.168.85.20" +WEB_ROOT="/var/www/html" + +echo "Building..." +cd "$FRONTEND_DIR" && npm run build + +echo "Deploying..." +ssh "$REMOTE" "rm -rf ${WEB_ROOT}/assets/*" +scp -r dist/* "$REMOTE:${WEB_ROOT}/" +ssh "$REMOTE" "chmod -R 755 ${WEB_ROOT}/assets && chmod 644 ${WEB_ROOT}/assets/* ${WEB_ROOT}/index.html ${WEB_ROOT}/vite.svg 2>/dev/null; nginx -s reload" +echo "Done." diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index b8b12df..6290418 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -315,6 +315,14 @@ class ApiClient { return this.del(`/api/v1/tasks/${id}`) } + runTaskNow(id) { + return this.post(`/api/v1/tasks/${id}/run`) + } + + updateTask(id, data) { + return this.put(`/api/v1/tasks/${id}`, data) + } + toggleTask(id) { return this.put(`/api/v1/tasks/${id}/toggle`) } diff --git a/frontend/src/components/AgentPanel.jsx b/frontend/src/components/AgentPanel.jsx index 791940c..c020988 100644 --- a/frontend/src/components/AgentPanel.jsx +++ b/frontend/src/components/AgentPanel.jsx @@ -6,7 +6,7 @@ import { useSettingsStore } from '../stores/settings' import { X, Cpu, MemoryStick, HardDrive, Clock, Network, Shield, Route, Globe, Key, Download, Terminal, Wifi, Search, Plus, Trash2, Copy, Check, ExternalLink, Unplug, Plug, MonitorSmartphone, Ban, - FileKey, UserCheck, Database, CalendarClock, Cable, Settings, Server, GitBranch, Play, Power, ChevronDown, ChevronRight, + FileKey, UserCheck, Database, CalendarClock, Cable, Settings, Server, GitBranch, Play, Power, ChevronDown, ChevronRight, Pencil, } from 'lucide-react' // Two-level navigation structure (DATAZONE style) @@ -1621,6 +1621,7 @@ function TasksTab({ agentId, agentName }) { const [total, setTotal] = useState(0) const [loading, setLoading] = useState(true) const [showDialog, setShowDialog] = useState(false) + const [editTask, setEditTask] = useState(null) const [creating, setCreating] = useState(false) const [error, setError] = useState('') const [showWebhook, setShowWebhook] = useState(false) @@ -1631,6 +1632,45 @@ function TasksTab({ agentId, agentName }) { reboot: false, health_check: false, health_check_timeout: 600, webhook_url: '', active: true, } + + const jobTemplates = [ + { + label: 'Wöchentliches Update', + icon: '🔄', + form: { name: 'Wöchentliches Update', action: 'update', schedule_type: 'weekly', + schedule_time: '02:00', schedule_weekday: 0, reboot: true, health_check: true, + health_check_timeout: 600, webhook_url: '', active: true }, + }, + { + label: 'Monatliches Update', + icon: '📅', + form: { name: 'Monatliches Update', action: 'update', schedule_type: 'monthly', + schedule_time: '02:00', schedule_monthday: 1, reboot: true, health_check: true, + health_check_timeout: 600, webhook_url: '', active: true }, + }, + { + label: 'Tägliches Backup', + icon: '💾', + form: { name: 'Tägliches Backup', action: 'backup', schedule_type: 'daily', + schedule_time: '03:00', reboot: false, health_check: false, + health_check_timeout: 600, webhook_url: '', active: true }, + }, + { + label: 'Monatliches Backup', + icon: '🗄️', + form: { name: 'Monatliches Backup', action: 'backup', schedule_type: 'monthly', + schedule_time: '03:00', schedule_monthday: 1, reboot: false, health_check: false, + health_check_timeout: 600, webhook_url: '', active: true }, + }, + { + label: 'Update + ERP-Webhook', + icon: '🔗', + form: { name: 'Monatliches Update + Servicebericht', action: 'update', schedule_type: 'monthly', + schedule_time: '02:00', schedule_monthday: 1, reboot: true, health_check: true, + health_check_timeout: 600, webhook_url: '', active: true }, + showWebhook: true, + }, + ] const [form, setForm] = useState(defaultForm) const loadTasks = () => { @@ -1669,9 +1709,47 @@ function TasksTab({ agentId, agentName }) { if (!confirm('Job wirklich loeschen?')) return try { await api.deleteTask(id); loadTasks() } catch (e) { setError(e.message) } } + const handleEdit = (t) => { + setEditTask(t) + setForm({ + name: t.name || '', + action: t.action, + schedule_type: t.schedule_type || 'once', + schedule_time: t.schedule_time || '02:00', + scheduled_at: '', + schedule_weekday: t.schedule_weekday ?? 1, + schedule_monthday: t.schedule_monthday ?? 1, + reboot: t.reboot || false, + health_check: t.health_check || false, + health_check_timeout: t.health_check_timeout || 600, + webhook_url: t.webhook_url || '', + active: t.active !== false, + }) + setShowWebhook(!!t.webhook_url) + setShowDialog(true) + } + + const handleSave = async () => { + setCreating(true); setError('') + try { + const data = { + name: form.name, schedule_type: form.schedule_type, schedule_time: form.schedule_time, + active: form.active, reboot: form.reboot, health_check: form.health_check, + health_check_timeout: parseInt(form.health_check_timeout) || 600, + webhook_url: form.webhook_url, + } + if (form.schedule_type === 'weekly') data.schedule_weekday = parseInt(form.schedule_weekday) + if (form.schedule_type === 'monthly') data.schedule_monthday = parseInt(form.schedule_monthday) + await api.updateTask(editTask.id, data) + setShowDialog(false); setEditTask(null); setForm(defaultForm); setShowWebhook(false); loadTasks() + } catch (e) { setError(e.message) } finally { setCreating(false) } + } + const handleRunNow = async (id) => { - // Trigger by creating a one-off copy? For now just toggle to trigger scheduler - // Simple approach: we don't have a "run now" endpoint, so we skip for now + try { + await api.runTaskNow(id) + setTimeout(loadTasks, 1500) + } catch (e) { setError(e.message) } } const weekdays = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'] @@ -1702,9 +1780,24 @@ function TasksTab({ agentId, agentName }) {
Vorlage wählen
+| Adresse | +Status | +Aktive Req. | +Fehler | +
|---|---|---|---|
| {u.address} | ++ {u.healthy ? 'Healthy' : 'Unhealthy'} + | +{u.num_requests} | +{u.fails} | +