WAU Compliance: agent collector, backend endpoints, frontend badge + panel
- Agent: WAU registry config check (include/exclude URLs), pending updates count - Backend: /wau/compliance per customer, /wau/compliance/summary all customers - Frontend: compliance badge in customer list, compliance panel in WAU tab - config.js: no longer gitignored (secrets-free, reads from env/window.__RMM_CONFIG__) - vite.config.js: reads backend URL from .env instead of hardcoded value - .env.example added for clean setup
This commit is contained in:
+55
-3
@@ -10,10 +10,14 @@ import (
|
||||
// setupWAURoutes registriert alle WAU-Endpunkte
|
||||
func (h *Handler) setupWAURoutes(mux *http.ServeMux) {
|
||||
// Authentifizierte Endpunkte (API-Key oder JWT)
|
||||
mux.HandleFunc("GET /api/v1/customers/{id}/wau/rules", h.listWAURules)
|
||||
mux.HandleFunc("POST /api/v1/customers/{id}/wau/rules", h.addWAURule)
|
||||
mux.HandleFunc("GET /api/v1/customers/{id}/wau/rules", h.listWAURules)
|
||||
mux.HandleFunc("POST /api/v1/customers/{id}/wau/rules", h.addWAURule)
|
||||
mux.HandleFunc("DELETE /api/v1/customers/{id}/wau/rules/{rid}", h.deleteWAURule)
|
||||
mux.HandleFunc("GET /api/v1/customers/{id}/wau/inventory", h.getWAUInventory)
|
||||
mux.HandleFunc("GET /api/v1/customers/{id}/wau/inventory", h.getWAUInventory)
|
||||
mux.HandleFunc("GET /api/v1/customers/{id}/wau/compliance", h.getWAUCompliance)
|
||||
|
||||
// Compliance-Summary für alle Kunden
|
||||
mux.HandleFunc("GET /api/v1/wau/compliance/summary", h.getWAUComplianceSummary)
|
||||
|
||||
// Plaintext-Endpunkte für WAU (kein Auth nötig — enthält nur App-IDs)
|
||||
mux.HandleFunc("GET /api/v1/customers/{id}/wau/included", h.wauIncludedList)
|
||||
@@ -149,3 +153,51 @@ func (h *Handler) serveWAUList(w http.ResponseWriter, r *http.Request, ruleType
|
||||
w.Write([]byte("\r\n"))
|
||||
}
|
||||
}
|
||||
|
||||
// wauExpectedURLs leitet die erwarteten WAU-URLs aus dem Request-Host ab
|
||||
func wauExpectedURLs(r *http.Request, customerID int) (includeURL, excludeURL string) {
|
||||
scheme := "https"
|
||||
if r.TLS == nil && (r.Header.Get("X-Forwarded-Proto") == "" || r.Header.Get("X-Forwarded-Proto") == "http") {
|
||||
scheme = "http"
|
||||
}
|
||||
if fwd := r.Header.Get("X-Forwarded-Proto"); fwd != "" {
|
||||
scheme = fwd
|
||||
}
|
||||
host := r.Host
|
||||
base := scheme + "://" + host
|
||||
includeURL = base + "/api/v1/customers/" + strconv.Itoa(customerID) + "/wau/included"
|
||||
excludeURL = base + "/api/v1/customers/" + strconv.Itoa(customerID) + "/wau/excluded"
|
||||
return
|
||||
}
|
||||
|
||||
// GET /api/v1/customers/{id}/wau/compliance
|
||||
func (h *Handler) getWAUCompliance(w http.ResponseWriter, r *http.Request) {
|
||||
customerID, err := strconv.Atoi(r.PathValue("id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültige Kunden-ID")
|
||||
return
|
||||
}
|
||||
includeURL, excludeURL := wauExpectedURLs(r, customerID)
|
||||
agents, err := h.db.GetWAUCompliance(customerID, includeURL, excludeURL)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Laden der Compliance-Daten")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"agents": agents,
|
||||
"include_url": includeURL,
|
||||
"exclude_url": excludeURL,
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/wau/compliance/summary — Zusammenfassung für alle Kunden
|
||||
func (h *Handler) getWAUComplianceSummary(w http.ResponseWriter, r *http.Request) {
|
||||
// Ohne customer_id können wir keine URLs ableiten — wir geben Summary ohne URL-Check zurück
|
||||
// oder nutzen customer_id=0 als Wildcard (kein URL-Check)
|
||||
summaries, err := h.db.GetAllCustomersWAUSummary("", "")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Laden der Compliance-Daten")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, summaries)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package db
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/cynfo/rmm-backend/models"
|
||||
)
|
||||
@@ -133,3 +134,217 @@ func (d *Database) GetWAUSoftwareInventory(customerID int) ([]models.WAUSoftware
|
||||
}
|
||||
return entries, rows.Err()
|
||||
}
|
||||
|
||||
// GetWAUCompliance berechnet den Compliance-Status aller Windows-Agents eines Kunden
|
||||
// Prüft: WAU installiert, URLs korrekt, alle allow-Pakete installiert
|
||||
func (d *Database) GetWAUCompliance(customerID int, expectedIncludeURL, expectedExcludeURL string) ([]models.WAUAgentCompliance, error) {
|
||||
// Alle Windows-Agents des Kunden mit ihren system_data holen
|
||||
rows, err := d.db.Query(`
|
||||
SELECT a.id, a.hostname, sd.data_json
|
||||
FROM agents a
|
||||
JOIN system_data sd ON sd.agent_id = a.id
|
||||
WHERE a.customer_id = $1
|
||||
AND sd.data_json ? 'windows'
|
||||
ORDER BY lower(a.hostname)
|
||||
`, customerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Allow-Pakete (= Required) für diesen Kunden laden
|
||||
allowRules, err := d.GetWAUList(customerID, "allow")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowRulesFull, err := d.ListWAURules(customerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Map winget_id → display_name
|
||||
displayNames := make(map[string]string)
|
||||
for _, r := range allowRulesFull {
|
||||
if r.RuleType == "allow" {
|
||||
displayNames[r.WingetID] = r.DisplayName
|
||||
}
|
||||
}
|
||||
|
||||
var result []models.WAUAgentCompliance
|
||||
|
||||
for rows.Next() {
|
||||
var agentID, hostname string
|
||||
var dataJSON []byte
|
||||
if err := rows.Scan(&agentID, &hostname, &dataJSON); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// JSON parsen (nur was wir brauchen)
|
||||
var data struct {
|
||||
Windows struct {
|
||||
InstalledSoftware []struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
} `json:"installed_software"`
|
||||
WAU *struct {
|
||||
Installed bool `json:"installed"`
|
||||
IncludeURL string `json:"include_url"`
|
||||
ExcludeURL string `json:"exclude_url"`
|
||||
PendingCount int `json:"pending_updates"`
|
||||
} `json:"wau"`
|
||||
} `json:"windows"`
|
||||
}
|
||||
if err := json.Unmarshal(dataJSON, &data); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ac := models.WAUAgentCompliance{
|
||||
AgentID: agentID,
|
||||
AgentName: hostname,
|
||||
}
|
||||
|
||||
// WAU-Status
|
||||
if data.Windows.WAU != nil {
|
||||
wau := data.Windows.WAU
|
||||
ac.WAUInstalled = wau.Installed
|
||||
ac.IncludeURL = wau.IncludeURL
|
||||
ac.ExcludeURL = wau.ExcludeURL
|
||||
ac.PendingCount = wau.PendingCount
|
||||
|
||||
// URL-Check: beide URLs müssen gesetzt sein und passen
|
||||
includeOK := expectedIncludeURL == "" || wau.IncludeURL == expectedIncludeURL
|
||||
excludeOK := expectedExcludeURL == "" || wau.ExcludeURL == expectedExcludeURL
|
||||
ac.WAUConfigOK = wau.Installed && includeOK && excludeOK
|
||||
}
|
||||
|
||||
// Installierte Software als lowercase-Set für schnelles Lookup
|
||||
installedSet := make(map[string]bool)
|
||||
for _, s := range data.Windows.InstalledSoftware {
|
||||
installedSet[strings.ToLower(s.Name)] = true
|
||||
}
|
||||
|
||||
// Pro allow-Paket prüfen ob installiert
|
||||
for _, wingetID := range allowRules {
|
||||
displayName := displayNames[wingetID]
|
||||
if displayName == "" {
|
||||
displayName = wingetID
|
||||
}
|
||||
pkg := models.WAUPackageCompliance{
|
||||
WingetID: wingetID,
|
||||
DisplayName: displayName,
|
||||
}
|
||||
// Fuzzy-Match: display_name (lowercase) muss in installierter Software enthalten sein
|
||||
lowerDisplay := strings.ToLower(displayName)
|
||||
for installedName := range installedSet {
|
||||
if strings.Contains(installedName, lowerDisplay) ||
|
||||
strings.Contains(lowerDisplay, installedName) {
|
||||
pkg.Installed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// Winget-ID-Teile als Fallback (z.B. "Mozilla.Firefox" → "firefox")
|
||||
if !pkg.Installed {
|
||||
parts := strings.Split(strings.ToLower(wingetID), ".")
|
||||
if len(parts) > 1 {
|
||||
appName := parts[len(parts)-1]
|
||||
for installedName := range installedSet {
|
||||
if strings.Contains(installedName, appName) {
|
||||
pkg.Installed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ac.Packages = append(ac.Packages, pkg)
|
||||
}
|
||||
|
||||
// Gesamt-Compliance: WAU installiert + korrekt + alle Pakete da + keine Updates ausstehend
|
||||
allInstalled := true
|
||||
for _, p := range ac.Packages {
|
||||
if !p.Installed {
|
||||
allInstalled = false
|
||||
break
|
||||
}
|
||||
}
|
||||
ac.Compliant = ac.WAUInstalled && ac.WAUConfigOK && allInstalled && ac.PendingCount == 0
|
||||
|
||||
result = append(result, ac)
|
||||
}
|
||||
if result == nil {
|
||||
result = []models.WAUAgentCompliance{}
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
// GetWAUComplianceSummary gibt eine kompakte Zusammenfassung für die Kundenliste zurück
|
||||
func (d *Database) GetWAUComplianceSummary(customerID int, expectedIncludeURL, expectedExcludeURL string) (*models.WAUComplianceSummary, error) {
|
||||
agents, err := d.GetWAUCompliance(customerID, expectedIncludeURL, expectedExcludeURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
summary := &models.WAUComplianceSummary{
|
||||
CustomerID: customerID,
|
||||
TotalAgents: len(agents),
|
||||
}
|
||||
|
||||
for _, a := range agents {
|
||||
if a.Compliant {
|
||||
summary.CompliantAgents++
|
||||
}
|
||||
if !a.WAUInstalled {
|
||||
summary.WAUMissing++
|
||||
} else if !a.WAUConfigOK {
|
||||
summary.WAUMisconfigured++
|
||||
}
|
||||
if a.PendingCount > 0 {
|
||||
summary.UpdatesPending++
|
||||
}
|
||||
for _, p := range a.Packages {
|
||||
if !p.Installed {
|
||||
summary.PackagesMissing++
|
||||
break // pro Agent nur einmal zählen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status ableiten
|
||||
if summary.WAUMissing > 0 || summary.PackagesMissing > 0 {
|
||||
summary.Status = "critical"
|
||||
} else if summary.WAUMisconfigured > 0 || summary.UpdatesPending > 0 {
|
||||
summary.Status = "warning"
|
||||
} else if summary.TotalAgents == 0 {
|
||||
summary.Status = "unknown"
|
||||
} else {
|
||||
summary.Status = "ok"
|
||||
}
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
// GetAllCustomersWAUSummary gibt Compliance-Summary für alle Kunden zurück
|
||||
func (d *Database) GetAllCustomersWAUSummary(includeURL, excludeURL string) ([]models.WAUComplianceSummary, error) {
|
||||
rows, err := d.db.Query(`SELECT id FROM customers ORDER BY name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var summaries []models.WAUComplianceSummary
|
||||
for rows.Next() {
|
||||
var id int
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
continue
|
||||
}
|
||||
s, err := d.GetWAUComplianceSummary(id, includeURL, excludeURL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if s.TotalAgents > 0 {
|
||||
summaries = append(summaries, *s)
|
||||
}
|
||||
}
|
||||
if summaries == nil {
|
||||
summaries = []models.WAUComplianceSummary{}
|
||||
}
|
||||
return summaries, rows.Err()
|
||||
}
|
||||
|
||||
@@ -17,3 +17,36 @@ type WAUSoftwareEntry struct {
|
||||
DeviceCount int `json:"device_count"`
|
||||
Versions []string `json:"versions"`
|
||||
}
|
||||
|
||||
// WAUPackageCompliance beschreibt den Compliance-Status eines einzelnen Pakets auf einem Gerät
|
||||
type WAUPackageCompliance struct {
|
||||
WingetID string `json:"winget_id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Installed bool `json:"installed"`
|
||||
HasUpdates bool `json:"has_updates"` // true wenn PendingCount > 0 und dieses Paket betroffen
|
||||
}
|
||||
|
||||
// WAUAgentCompliance beschreibt den vollständigen WAU-Compliance-Status eines Agents
|
||||
type WAUAgentCompliance struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
AgentName string `json:"agent_name"`
|
||||
WAUInstalled bool `json:"wau_installed"`
|
||||
WAUConfigOK bool `json:"wau_config_ok"` // Include/Exclude-URLs korrekt gesetzt
|
||||
IncludeURL string `json:"include_url"` // tatsächlich konfigurierte URL
|
||||
ExcludeURL string `json:"exclude_url"` // tatsächlich konfigurierte URL
|
||||
PendingCount int `json:"pending_updates"`
|
||||
Packages []WAUPackageCompliance `json:"packages"`
|
||||
Compliant bool `json:"compliant"` // true wenn alles OK
|
||||
}
|
||||
|
||||
// WAUComplianceSummary fasst den Compliance-Status eines Kunden zusammen (für die Kundenliste)
|
||||
type WAUComplianceSummary struct {
|
||||
CustomerID int `json:"customer_id"`
|
||||
TotalAgents int `json:"total_agents"`
|
||||
CompliantAgents int `json:"compliant_agents"`
|
||||
WAUMissing int `json:"wau_missing"` // Agents ohne WAU
|
||||
WAUMisconfigured int `json:"wau_misconfigured"` // WAU installiert, aber falsche URLs
|
||||
PackagesMissing int `json:"packages_missing"` // Agents mit fehlenden Pflichtpaketen
|
||||
UpdatesPending int `json:"updates_pending"` // Agents mit ausstehenden Updates
|
||||
Status string `json:"status"` // "ok" | "warning" | "critical"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user