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:
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user