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)
This commit is contained in:
@@ -280,6 +280,18 @@ func (d *Database) migrate() error {
|
||||
d.db.Exec(`ALTER TABLE tasks ADD COLUMN IF NOT EXISTS script_timeout INT DEFAULT 300`)
|
||||
d.db.Exec(`CREATE INDEX IF NOT EXISTS idx_tasks_active_next_run ON tasks(active, next_run) WHERE active = TRUE`)
|
||||
|
||||
// WAU-Regeln pro Kunde
|
||||
d.db.Exec(`CREATE TABLE IF NOT EXISTS customer_wau_rules (
|
||||
id SERIAL PRIMARY KEY,
|
||||
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
||||
winget_id TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL DEFAULT '',
|
||||
rule_type TEXT NOT NULL CHECK (rule_type IN ('allow', 'block')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(customer_id, winget_id)
|
||||
)`)
|
||||
d.db.Exec(`CREATE INDEX IF NOT EXISTS idx_wau_rules_customer ON customer_wau_rules(customer_id, rule_type)`)
|
||||
|
||||
// Retention Policy (90 Tage)
|
||||
d.setupRetention()
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user