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:
cynfo3000
2026-03-10 17:38:58 +01:00
parent bf51ed8569
commit 54ddc0b902
12 changed files with 677 additions and 19 deletions
+215
View File
@@ -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()
}