rmm2/backend/db/wau.go
cynfo3000 bf51ed8569 feat: WAU-Integration + Windows Software-Inventar (v1.1.0)
Windows Agent (v1.1.0):
- Software-Inventar aus Windows-Registry (64-bit + 32-bit)
  Sammelt Name, Version, Hersteller, InstallDate aller installierten Apps
  Filtert SystemComponent-Einträge heraus, sortiert alphabetisch
  Funktioniert unter SYSTEM-Account (kein winget nötig)
- Version auf 1.1.0 gehoben

Backend:
- customer_wau_rules Tabelle (Migration automatisch)
- WAU-Regeln pro Kunde (allow/block, winget-ID + Anzeigename)
- GET/POST/DELETE /api/v1/customers/{id}/wau/rules
- GET /api/v1/customers/{id}/wau/inventory  (SQL-Aggregation aus Agenten-Daten)
- GET /api/v1/customers/{id}/wau/included   (Plaintext für WAU)
- GET /api/v1/customers/{id}/wau/excluded   (Plaintext für WAU)

Frontend:
- Kunden-Seite: Tab-Navigation (Agents | API-Keys | WAU)
- WAU-Tab pro Kunde: Allowlist + Blocklist mit Inline-Assign aus Inventar
  Allow/Block-Buttons direkt in Inventar-Zeile, winget-ID vorgeschlagen
  Installationsbefehl mit externer Backend-URL aus Systemeinstellungen
- Windows-Panel Software-Tab: Installiert-Liste aus Registry-Daten
- Windows-Panel WAU-Tab: Status (installiert/Task/letzter Lauf)
  WAU installieren via MSI-Download + msiexec (synchron)
  Jetzt ausführen, Log anzeigen
  Gewünschten Zustand herstellen (Allowlist auf Client installieren)
2026-03-10 08:46:00 +01:00

136 lines
4.0 KiB
Go

package db
import (
"database/sql"
"encoding/json"
"github.com/cynfo/rmm-backend/models"
)
// ListWAURules gibt alle WAU-Regeln eines Kunden zurück
func (d *Database) ListWAURules(customerID int) ([]models.WAURule, error) {
rows, err := d.db.Query(`
SELECT id, customer_id, winget_id, display_name, rule_type, created_at
FROM customer_wau_rules
WHERE customer_id = $1
ORDER BY rule_type, lower(display_name)
`, customerID)
if err != nil {
return nil, err
}
defer rows.Close()
var rules []models.WAURule
for rows.Next() {
var r models.WAURule
if err := rows.Scan(&r.ID, &r.CustomerID, &r.WingetID, &r.DisplayName, &r.RuleType, &r.CreatedAt); err != nil {
return nil, err
}
rules = append(rules, r)
}
if rules == nil {
rules = []models.WAURule{}
}
return rules, rows.Err()
}
// AddWAURule fügt eine neue Regel hinzu (upsert auf winget_id)
func (d *Database) AddWAURule(customerID int, wingetID, displayName, ruleType string) (*models.WAURule, error) {
var r models.WAURule
err := d.db.QueryRow(`
INSERT INTO customer_wau_rules (customer_id, winget_id, display_name, rule_type)
VALUES ($1, $2, $3, $4)
ON CONFLICT (customer_id, winget_id) DO UPDATE
SET display_name = EXCLUDED.display_name,
rule_type = EXCLUDED.rule_type
RETURNING id, customer_id, winget_id, display_name, rule_type, created_at
`, customerID, wingetID, displayName, ruleType).Scan(
&r.ID, &r.CustomerID, &r.WingetID, &r.DisplayName, &r.RuleType, &r.CreatedAt,
)
if err != nil {
return nil, err
}
return &r, nil
}
// DeleteWAURule löscht eine Regel (per ID, gehört zu customerID)
func (d *Database) DeleteWAURule(customerID, ruleID int) (bool, error) {
res, err := d.db.Exec(`
DELETE FROM customer_wau_rules WHERE id = $1 AND customer_id = $2
`, ruleID, customerID)
if err != nil {
return false, err
}
n, _ := res.RowsAffected()
return n > 0, nil
}
// GetWAUList gibt alle winget-IDs eines bestimmten Typs zurück (für den Plaintext-Endpoint)
func (d *Database) GetWAUList(customerID int, ruleType string) ([]string, error) {
rows, err := d.db.Query(`
SELECT winget_id FROM customer_wau_rules
WHERE customer_id = $1 AND rule_type = $2
ORDER BY lower(winget_id)
`, customerID, ruleType)
if err != nil {
return nil, err
}
defer rows.Close()
var ids []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, err
}
ids = append(ids, id)
}
return ids, rows.Err()
}
// GetWAUSoftwareInventory aggregiert installierte Software aller Windows-Agents eines Kunden
func (d *Database) GetWAUSoftwareInventory(customerID int) ([]models.WAUSoftwareEntry, error) {
rows, err := d.db.Query(`
SELECT
app->>'name' AS name,
COALESCE(app->>'publisher', '') AS publisher,
COUNT(DISTINCT sd.agent_id) AS device_count,
jsonb_agg(DISTINCT app->>'version') FILTER (WHERE (app->>'version') != '') AS versions
FROM agents a
JOIN system_data sd ON sd.agent_id = a.id
CROSS JOIN jsonb_array_elements(sd.data_json->'windows'->'installed_software') AS app
WHERE a.customer_id = $1
AND sd.data_json ? 'windows'
AND jsonb_typeof(sd.data_json->'windows'->'installed_software') = 'array'
AND (app->>'name') IS NOT NULL
AND (app->>'name') != ''
GROUP BY app->>'name', app->>'publisher'
ORDER BY lower(app->>'name')
`, customerID)
if err != nil {
return nil, err
}
defer rows.Close()
var entries []models.WAUSoftwareEntry
for rows.Next() {
var e models.WAUSoftwareEntry
var versionsJSON sql.NullString
if err := rows.Scan(&e.Name, &e.Publisher, &e.DeviceCount, &versionsJSON); err != nil {
return nil, err
}
if versionsJSON.Valid && versionsJSON.String != "" && versionsJSON.String != "null" {
json.Unmarshal([]byte(versionsJSON.String), &e.Versions)
}
if e.Versions == nil {
e.Versions = []string{}
}
entries = append(entries, e)
}
if entries == nil {
entries = []models.WAUSoftwareEntry{}
}
return entries, rows.Err()
}